Commit b78569f6 authored by rajeshsubhankar's avatar rajeshsubhankar Committed by Marc Gravell

Feature/NRediSearch-TAGS (#749)

* Tag feature added
parent e0810eed
// .NET port of https://github.com/RedisLabs/JRediSearch/ // .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis; using StackExchange.Redis;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace NRediSearch namespace NRediSearch
{ {
public sealed class Client public sealed class Client
{ {
[Flags] [Flags]
public enum IndexOptions public enum IndexOptions
{ {
/// <summary> /// <summary>
/// All options disabled /// All options disabled
/// </summary> /// </summary>
None = 0, None = 0,
/// <summary> /// <summary>
/// Set this to tell the index not to save term offset vectors. This reduces memory consumption but does not /// Set this to tell the index not to save term offset vectors. This reduces memory consumption but does not
/// allow performing exact matches, and reduces overall relevance of multi-term queries /// allow performing exact matches, and reduces overall relevance of multi-term queries
/// </summary> /// </summary>
UseTermOffsets = 1, UseTermOffsets = 1,
/// <summary> /// <summary>
/// If set (default), we keep flags per index record telling us what fields the term appeared on, /// If set (default), we keep flags per index record telling us what fields the term appeared on,
/// and allowing us to filter results by field /// and allowing us to filter results by field
/// </summary> /// </summary>
KeepFieldFlags = 2, KeepFieldFlags = 2,
/// <summary> /// <summary>
/// If set, we keep an index of the top entries per term, allowing extremely fast single word queries /// If set, we keep an index of the top entries per term, allowing extremely fast single word queries
/// regardless of index size, at the cost of more memory /// regardless of index size, at the cost of more memory
/// </summary> /// </summary>
UseScoreIndexes = 4, UseScoreIndexes = 4,
/// <summary> /// <summary>
/// The default indexing options - use term offsets and keep fields flags /// The default indexing options - use term offsets and keep fields flags
/// </summary> /// </summary>
Default = UseTermOffsets | KeepFieldFlags Default = UseTermOffsets | KeepFieldFlags
} }
private static void SerializeRedisArgs(IndexOptions flags, List<object> args) private static void SerializeRedisArgs(IndexOptions flags, List<object> args)
{ {
if ((flags & IndexOptions.UseTermOffsets) == 0) if ((flags & IndexOptions.UseTermOffsets) == 0)
{ {
args.Add("NOOFFSETS".Literal()); args.Add("NOOFFSETS".Literal());
} }
if ((flags & IndexOptions.KeepFieldFlags) == 0) if ((flags & IndexOptions.KeepFieldFlags) == 0)
{ {
args.Add("NOFIELDS".Literal()); args.Add("NOFIELDS".Literal());
} }
if ((flags & IndexOptions.UseScoreIndexes) == 0) if ((flags & IndexOptions.UseScoreIndexes) == 0)
{ {
args.Add("NOSCOREIDX".Literal()); args.Add("NOSCOREIDX".Literal());
} }
} }
private readonly IDatabaseAsync _db; private readonly IDatabaseAsync _db;
private IDatabase DbSync private IDatabase DbSync
=> (_db as IDatabase) ?? throw new InvalidOperationException("Synchronous operations are not available on this database instance"); => (_db as IDatabase) ?? throw new InvalidOperationException("Synchronous operations are not available on this database instance");
private readonly object _boxedIndexName; private readonly object _boxedIndexName;
public RedisKey IndexName => (RedisKey)_boxedIndexName; public RedisKey IndexName => (RedisKey)_boxedIndexName;
public Client(RedisKey indexName, IDatabaseAsync db) public Client(RedisKey indexName, IDatabaseAsync db)
{ {
_db = db ?? throw new ArgumentNullException(nameof(db)); _db = db ?? throw new ArgumentNullException(nameof(db));
_boxedIndexName = indexName; // only box once, not per-command _boxedIndexName = indexName; // only box once, not per-command
} }
public Client(RedisKey indexName, IDatabase db) : this(indexName, (IDatabaseAsync)db) { } public Client(RedisKey indexName, IDatabase db) : this(indexName, (IDatabaseAsync)db) { }
/// <summary> /// <summary>
/// Create the index definition in redis /// Create the index definition in redis
/// </summary> /// </summary>
/// <param name="schema">a schema definition <seealso cref="Schema"/></param> /// <param name="schema">a schema definition <seealso cref="Schema"/></param>
/// <param name="options">index option flags <seealso cref="IndexOptions"/></param> /// <param name="options">index option flags <seealso cref="IndexOptions"/></param>
/// <returns>true if successful</returns> /// <returns>true if successful</returns>
public bool CreateIndex(Schema schema, IndexOptions options) public bool CreateIndex(Schema schema, IndexOptions options)
{ {
var args = new List<object>(); var args = new List<object>();
args.Add(_boxedIndexName); args.Add(_boxedIndexName);
SerializeRedisArgs(options, args); SerializeRedisArgs(options, args);
args.Add("SCHEMA".Literal()); args.Add("SCHEMA".Literal());
foreach (var f in schema.Fields) foreach (var f in schema.Fields)
{ {
f.SerializeRedisArgs(args); f.SerializeRedisArgs(args);
} }
return (string)DbSync.Execute("FT.CREATE", args) == "OK"; return (string)DbSync.Execute("FT.CREATE", args) == "OK";
} }
/// <summary> /// <summary>
/// Create the index definition in redis /// Create the index definition in redis
/// </summary> /// </summary>
/// <param name="schema">a schema definition <seealso cref="Schema"/></param> /// <param name="schema">a schema definition <seealso cref="Schema"/></param>
/// <param name="options">index option flags <seealso cref="IndexOptions"/></param> /// <param name="options">index option flags <seealso cref="IndexOptions"/></param>
/// <returns>true if successful</returns> /// <returns>true if successful</returns>
public async Task<bool> CreateIndexAsync(Schema schema, IndexOptions options) public async Task<bool> CreateIndexAsync(Schema schema, IndexOptions options)
{ {
var args = new List<object>(); var args = new List<object>();
args.Add(_boxedIndexName); args.Add(_boxedIndexName);
SerializeRedisArgs(options, args); SerializeRedisArgs(options, args);
args.Add("SCHEMA".Literal()); args.Add("SCHEMA".Literal());
foreach (var f in schema.Fields) foreach (var f in schema.Fields)
{ {
f.SerializeRedisArgs(args); f.SerializeRedisArgs(args);
} }
return (string)await _db.ExecuteAsync("FT.CREATE", args).ConfigureAwait(false) == "OK"; return (string)await _db.ExecuteAsync("FT.CREATE", args).ConfigureAwait(false) == "OK";
} }
/// <summary> /// <summary>
/// Search the index /// Search the index
/// </summary> /// </summary>
/// <param name="q">a <see cref="Query"/> object with the query string and optional parameters</param> /// <param name="q">a <see cref="Query"/> object with the query string and optional parameters</param>
/// <returns>a <see cref="SearchResult"/> object with the results</returns> /// <returns>a <see cref="SearchResult"/> object with the results</returns>
public SearchResult Search(Query q) public SearchResult Search(Query q)
{ {
var args = new List<object>(); var args = new List<object>();
args.Add(_boxedIndexName); args.Add(_boxedIndexName);
q.SerializeRedisArgs(args); q.SerializeRedisArgs(args);
var resp = (RedisResult[])DbSync.Execute("FT.SEARCH", args); var resp = (RedisResult[])DbSync.Execute("FT.SEARCH", args);
return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads); return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads);
} }
/// <summary> /// <summary>
/// Search the index /// Search the index
/// </summary> /// </summary>
/// <param name="q">a <see cref="Query"/> object with the query string and optional parameters</param> /// <param name="q">a <see cref="Query"/> object with the query string and optional parameters</param>
/// <returns>a <see cref="SearchResult"/> object with the results</returns> /// <returns>a <see cref="SearchResult"/> object with the results</returns>
public async Task<SearchResult> SearchAsync(Query q) public async Task<SearchResult> SearchAsync(Query q)
{ {
var args = new List<object>(); var args = new List<object>();
args.Add(_boxedIndexName); args.Add(_boxedIndexName);
q.SerializeRedisArgs(args); q.SerializeRedisArgs(args);
var resp = (RedisResult[])await _db.ExecuteAsync("FT.SEARCH", args).ConfigureAwait(false); var resp = (RedisResult[])await _db.ExecuteAsync("FT.SEARCH", args).ConfigureAwait(false);
return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads); return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads);
} }
/// <summary> /// <summary>
/// Add a single document to the query /// Return Distinct Values in a TAG field
/// </summary> /// </summary>
/// <param name="docId">the id of the document. It cannot belong to a document already in the index unless replace is set</param> /// <param name="fieldName">TAG field name</param>
/// <param name="score">the document's score, floating point number between 0 and 1</param> /// <returns>List of TAG field values</returns>
/// <param name="fields">a map of the document's fields</param> public RedisValue[] TagVals(string fieldName)
/// <param name="noSave">if set, we only index the document and do not save its contents. This allows fetching just doc ids</param> {
/// <param name="replace">if set, and the document already exists, we reindex and update it</param> var resp = (RedisValue[])DbSync.Execute("FT.TAGVALS", _boxedIndexName, fieldName);
/// <param name="payload">if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server</param>
public bool AddDocument(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null) return resp;
{ }
var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload);
return (string)DbSync.Execute("FT.ADD", args) == "OK"; /// <summary>
} /// Return Distinct Values in a TAG field
/// </summary>
/// <summary> /// <param name="fieldName">TAG field name</param>
/// Add a single document to the query /// <returns>List of TAG field values</returns>
/// </summary> public async Task<RedisValue[]> TagValsAsync(string fieldName)
/// <param name="docId">the id of the document. It cannot belong to a document already in the index unless replace is set</param> {
/// <param name="score">the document's score, floating point number between 0 and 1</param> var resp = (RedisValue[])await _db.ExecuteAsync("FT.TAGVALS", _boxedIndexName, fieldName).ConfigureAwait(false);
/// <param name="fields">a map of the document's fields</param>
/// <param name="noSave">if set, we only index the document and do not save its contents. This allows fetching just doc ids</param> return resp;
/// <param name="replace">if set, and the document already exists, we reindex and update it</param> }
/// <param name="payload">if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server</param>
public async Task<bool> AddDocumentAsync(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null)
{ /// <summary>
var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload); /// Add a single document to the query
return (string)await _db.ExecuteAsync("FT.ADD", args).ConfigureAwait(false) == "OK"; /// </summary>
} /// <param name="docId">the id of the document. It cannot belong to a document already in the index unless replace is set</param>
/// <param name="score">the document's score, floating point number between 0 and 1</param>
private List<object> BuildAddDocumentArgs(string docId, Dictionary<string, RedisValue> fields, double score, bool noSave, bool replace, byte[] payload) /// <param name="fields">a map of the document's fields</param>
{ /// <param name="noSave">if set, we only index the document and do not save its contents. This allows fetching just doc ids</param>
var args = new List<object> { _boxedIndexName, docId, score }; /// <param name="replace">if set, and the document already exists, we reindex and update it</param>
if (noSave) /// <param name="payload">if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server</param>
{ public bool AddDocument(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null)
args.Add("NOSAVE".Literal()); {
} var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload);
if (replace) return (string)DbSync.Execute("FT.ADD", args) == "OK";
{ }
args.Add("REPLACE".Literal());
} /// <summary>
if (payload != null) /// Add a single document to the query
{ /// </summary>
args.Add("PAYLOAD".Literal()); /// <param name="docId">the id of the document. It cannot belong to a document already in the index unless replace is set</param>
// TODO: Fix this /// <param name="score">the document's score, floating point number between 0 and 1</param>
args.Add(payload); /// <param name="fields">a map of the document's fields</param>
} /// <param name="noSave">if set, we only index the document and do not save its contents. This allows fetching just doc ids</param>
/// <param name="replace">if set, and the document already exists, we reindex and update it</param>
args.Add("FIELDS".Literal()); /// <param name="payload">if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server</param>
foreach (var ent in fields) public async Task<bool> AddDocumentAsync(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null)
{ {
args.Add(ent.Key); var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload);
args.Add(ent.Value); return (string)await _db.ExecuteAsync("FT.ADD", args).ConfigureAwait(false) == "OK";
} }
return args;
} private List<object> BuildAddDocumentArgs(string docId, Dictionary<string, RedisValue> fields, double score, bool noSave, bool replace, byte[] payload)
{
/// <summary> var args = new List<object> { _boxedIndexName, docId, score };
/// replaceDocument is a convenience for calling addDocument with replace=true if (noSave)
/// </summary> {
public bool ReplaceDocument(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, byte[] payload = null) args.Add("NOSAVE".Literal());
=> AddDocument(docId, fields, score, false, true, payload); }
if (replace)
/// <summary> {
/// replaceDocument is a convenience for calling addDocument with replace=true args.Add("REPLACE".Literal());
/// </summary> }
public Task<bool> ReplaceDocumentAsync(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, byte[] payload = null) if (payload != null)
=> AddDocumentAsync(docId, fields, score, false, true, payload); {
args.Add("PAYLOAD".Literal());
/// <summary> // TODO: Fix this
/// Index a document already in redis as a HASH key. args.Add(payload);
/// </summary> }
/// <param name="docId">the id of the document in redis. This must match an existing, unindexed HASH key</param>
/// <param name="score">the document's index score, between 0 and 1</param> args.Add("FIELDS".Literal());
/// <param name="replace">if set, and the document already exists, we reindex and update it</param> foreach (var ent in fields)
/// <returns>true on success</returns> {
public bool AddHash(string docId, double score, bool replace) args.Add(ent.Key);
{ args.Add(ent.Value);
var args = new List<object> { _boxedIndexName, docId, score }; }
if (replace) return args;
{ }
args.Add("REPLACE".Literal());
} /// <summary>
return (string)DbSync.Execute("FT.ADDHASH", args) == "OK"; /// replaceDocument is a convenience for calling addDocument with replace=true
} /// </summary>
/// <summary> public bool ReplaceDocument(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, byte[] payload = null)
/// Index a document already in redis as a HASH key. => AddDocument(docId, fields, score, false, true, payload);
/// </summary>
/// <param name="docId">the id of the document in redis. This must match an existing, unindexed HASH key</param> /// <summary>
/// <param name="score">the document's index score, between 0 and 1</param> /// replaceDocument is a convenience for calling addDocument with replace=true
/// <param name="replace">if set, and the document already exists, we reindex and update it</param> /// </summary>
/// <returns>true on success</returns> public Task<bool> ReplaceDocumentAsync(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, byte[] payload = null)
public async Task<bool> AddHashAsync(string docId, double score, bool replace) => AddDocumentAsync(docId, fields, score, false, true, payload);
{
var args = new List<object> { _boxedIndexName, docId, score }; /// <summary>
if (replace) /// Index a document already in redis as a HASH key.
{ /// </summary>
args.Add("REPLACE".Literal()); /// <param name="docId">the id of the document in redis. This must match an existing, unindexed HASH key</param>
} /// <param name="score">the document's index score, between 0 and 1</param>
return (string)await _db.ExecuteAsync("FT.ADDHASH", args).ConfigureAwait(false) == "OK"; /// <param name="replace">if set, and the document already exists, we reindex and update it</param>
} /// <returns>true on success</returns>
public bool AddHash(string docId, double score, bool replace)
/// <summary> {
/// Get the index info, including memory consumption and other statistics. var args = new List<object> { _boxedIndexName, docId, score };
/// </summary> if (replace)
/// <remarks>TODO: Make a class for easier access to the index properties</remarks> {
/// <returns>a map of key/value pairs</returns> args.Add("REPLACE".Literal());
public Dictionary<string, RedisValue> GetInfo() }
{ return (string)DbSync.Execute("FT.ADDHASH", args) == "OK";
return ParseGetInfo(DbSync.Execute("FT.INFO", _boxedIndexName)); }
} /// <summary>
/// <summary> /// Index a document already in redis as a HASH key.
/// Get the index info, including memory consumption and other statistics. /// </summary>
/// </summary> /// <param name="docId">the id of the document in redis. This must match an existing, unindexed HASH key</param>
/// <remarks>TODO: Make a class for easier access to the index properties</remarks> /// <param name="score">the document's index score, between 0 and 1</param>
/// <returns>a map of key/value pairs</returns> /// <param name="replace">if set, and the document already exists, we reindex and update it</param>
public async Task<Dictionary<string, RedisValue>> GetInfoAsync() /// <returns>true on success</returns>
{ public async Task<bool> AddHashAsync(string docId, double score, bool replace)
return ParseGetInfo(await _db.ExecuteAsync("FT.INFO", _boxedIndexName).ConfigureAwait(false)); {
} var args = new List<object> { _boxedIndexName, docId, score };
static Dictionary<string, RedisValue> ParseGetInfo(RedisResult value) if (replace)
{ {
var res = (RedisValue[])value; args.Add("REPLACE".Literal());
var info = new Dictionary<string, RedisValue>(); }
for (int i = 0; i < res.Length; i += 2) return (string)await _db.ExecuteAsync("FT.ADDHASH", args).ConfigureAwait(false) == "OK";
{ }
var key = (string)res[i];
var val = res[i + 1]; /// <summary>
info.Add(key, val); /// Get the index info, including memory consumption and other statistics.
} /// </summary>
return info; /// <remarks>TODO: Make a class for easier access to the index properties</remarks>
} /// <returns>a map of key/value pairs</returns>
public Dictionary<string, RedisValue> GetInfo()
/// <summary> {
/// Delete a document from the index. return ParseGetInfo(DbSync.Execute("FT.INFO", _boxedIndexName));
/// </summary> }
/// <param name="docId">the document's id</param> /// <summary>
/// <returns>true if it has been deleted, false if it did not exist</returns> /// Get the index info, including memory consumption and other statistics.
public bool DeleteDocument(string docId) /// </summary>
{ /// <remarks>TODO: Make a class for easier access to the index properties</remarks>
return (long)DbSync.Execute("FT.DEL", _boxedIndexName, docId) == 1; /// <returns>a map of key/value pairs</returns>
} public async Task<Dictionary<string, RedisValue>> GetInfoAsync()
{
/// <summary> return ParseGetInfo(await _db.ExecuteAsync("FT.INFO", _boxedIndexName).ConfigureAwait(false));
/// Delete a document from the index. }
/// </summary> static Dictionary<string, RedisValue> ParseGetInfo(RedisResult value)
/// <param name="docId">the document's id</param> {
/// <returns>true if it has been deleted, false if it did not exist</returns> var res = (RedisValue[])value;
public async Task<bool> DeleteDocumentAsync(string docId) var info = new Dictionary<string, RedisValue>();
{ for (int i = 0; i < res.Length; i += 2)
return (long)await _db.ExecuteAsync("FT.DEL", _boxedIndexName, docId).ConfigureAwait(false) == 1; {
} var key = (string)res[i];
var val = res[i + 1];
/// <summary> info.Add(key, val);
/// Drop the index and all associated keys, including documents }
/// </summary> return info;
/// <returns>true on success</returns> }
public bool DropIndex()
{ /// <summary>
return (string)DbSync.Execute("FT.DROP", _boxedIndexName) == "OK"; /// Delete a document from the index.
} /// </summary>
/// <summary> /// <param name="docId">the document's id</param>
/// Drop the index and all associated keys, including documents /// <returns>true if it has been deleted, false if it did not exist</returns>
/// </summary> public bool DeleteDocument(string docId)
/// <returns>true on success</returns> {
public async Task<bool> DropIndexAsync() return (long)DbSync.Execute("FT.DEL", _boxedIndexName, docId) == 1;
{ }
return (string) await _db.ExecuteAsync("FT.DROP", _boxedIndexName).ConfigureAwait(false) == "OK";
} /// <summary>
/// Delete a document from the index.
/// <summary> /// </summary>
/// Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed /// <param name="docId">the document's id</param>
/// </summary> /// <returns>true if it has been deleted, false if it did not exist</returns>
public long OptimizeIndex() public async Task<bool> DeleteDocumentAsync(string docId)
{ {
return (long)DbSync.Execute("FT.OPTIMIZE", _boxedIndexName); return (long)await _db.ExecuteAsync("FT.DEL", _boxedIndexName, docId).ConfigureAwait(false) == 1;
} }
/// <summary> /// <summary>
/// Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed /// Drop the index and all associated keys, including documents
/// </summary> /// </summary>
public async Task<long> OptimizeIndexAsync() /// <returns>true on success</returns>
{ public bool DropIndex()
return (long) await _db.ExecuteAsync("FT.OPTIMIZE", _boxedIndexName).ConfigureAwait(false); {
} return (string)DbSync.Execute("FT.DROP", _boxedIndexName) == "OK";
}
/// <summary> /// <summary>
/// Get the size of an autoc-complete suggestion dictionary /// Drop the index and all associated keys, including documents
/// </summary> /// </summary>
public long CountSuggestions() /// <returns>true on success</returns>
=> (long)DbSync.Execute("FT.SUGLEN", _boxedIndexName); public async Task<bool> DropIndexAsync()
{
/// <summary> return (string) await _db.ExecuteAsync("FT.DROP", _boxedIndexName).ConfigureAwait(false) == "OK";
/// Get the size of an autoc-complete suggestion dictionary }
/// </summary>
public async Task<long> CountSuggestionsAsync() /// <summary>
=> (long)await _db.ExecuteAsync("FT.SUGLEN", _boxedIndexName).ConfigureAwait(false); /// Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed
/// </summary>
/// <summary> public long OptimizeIndex()
/// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user. {
/// </summary> return (long)DbSync.Execute("FT.OPTIMIZE", _boxedIndexName);
/// <param name="value">the suggestion string we index</param> }
/// <param name="score">a floating point number of the suggestion string's weight</param>
/// <param name="increment">if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time</param> /// <summary>
/// <returns>the current size of the suggestion dictionary.</returns> /// Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed
public long AddSuggestion(string value, double score, bool increment = false) /// </summary>
{ public async Task<long> OptimizeIndexAsync()
object args = increment {
? new object[] { _boxedIndexName, value, score, "INCR".Literal() } return (long) await _db.ExecuteAsync("FT.OPTIMIZE", _boxedIndexName).ConfigureAwait(false);
: new object[] { _boxedIndexName, value, score }; }
return (long)DbSync.Execute("FT.SUGADD", args);
} /// <summary>
/// Get the size of an autoc-complete suggestion dictionary
/// <summary> /// </summary>
/// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user. public long CountSuggestions()
/// </summary> => (long)DbSync.Execute("FT.SUGLEN", _boxedIndexName);
/// <param name="value">the suggestion string we index</param>
/// <param name="score">a floating point number of the suggestion string's weight</param> /// <summary>
/// <param name="increment">if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time</param> /// Get the size of an autoc-complete suggestion dictionary
/// <returns>the current size of the suggestion dictionary.</returns> /// </summary>
public async Task<long> AddSuggestionAsync(string value, double score, bool increment = false) public async Task<long> CountSuggestionsAsync()
{ => (long)await _db.ExecuteAsync("FT.SUGLEN", _boxedIndexName).ConfigureAwait(false);
object args = increment
? new object[] { _boxedIndexName, value, score, "INCR".Literal() } /// <summary>
: new object[] { _boxedIndexName, value, score }; /// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user.
return (long)await _db.ExecuteAsync("FT.SUGADD", args).ConfigureAwait(false); /// </summary>
} /// <param name="value">the suggestion string we index</param>
/// <param name="score">a floating point number of the suggestion string's weight</param>
/// <summary> /// <param name="increment">if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time</param>
/// Delete a string from a suggestion index. /// <returns>the current size of the suggestion dictionary.</returns>
/// </summary> public long AddSuggestion(string value, double score, bool increment = false)
/// <param name="value">the string to delete</param> {
public bool DeleteSuggestion(string value) object args = increment
=> (long)DbSync.Execute("FT.SUGDEL", _boxedIndexName, value) == 1; ? new object[] { _boxedIndexName, value, score, "INCR".Literal() }
: new object[] { _boxedIndexName, value, score };
/// <summary> return (long)DbSync.Execute("FT.SUGADD", args);
/// Delete a string from a suggestion index. }
/// </summary>
/// <param name="value">the string to delete</param> /// <summary>
public async Task<bool> DeleteSuggestionAsync(string value) /// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user.
=> (long)await _db.ExecuteAsync("FT.SUGDEL", _boxedIndexName, value).ConfigureAwait(false) == 1; /// </summary>
/// <param name="value">the suggestion string we index</param>
/// <summary> /// <param name="score">a floating point number of the suggestion string's weight</param>
/// Get completion suggestions for a prefix /// <param name="increment">if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time</param>
/// </summary> /// <returns>the current size of the suggestion dictionary.</returns>
/// <param name="prefix">the prefix to complete on</param> public async Task<long> AddSuggestionAsync(string value, double score, bool increment = false)
/// <param name="fuzzy"> if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent</param> {
/// <param name="max">If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10).</param> object args = increment
/// <returns>a list of the top suggestions matching the prefix</returns> ? new object[] { _boxedIndexName, value, score, "INCR".Literal() }
public string[] GetSuggestions(string prefix, bool fuzzy = false, int max = 5) : new object[] { _boxedIndexName, value, score };
{ return (long)await _db.ExecuteAsync("FT.SUGADD", args).ConfigureAwait(false);
var args = new List<object> { _boxedIndexName, prefix}; }
if (fuzzy) args.Add("FUZZY".Literal());
if (max != 5) /// <summary>
{ /// Delete a string from a suggestion index.
args.Add("MAX".Literal()); /// </summary>
args.Add(max); /// <param name="value">the string to delete</param>
} public bool DeleteSuggestion(string value)
return (string[])DbSync.Execute("FT.SUGGET", args); => (long)DbSync.Execute("FT.SUGDEL", _boxedIndexName, value) == 1;
}
/// <summary> /// <summary>
/// Get completion suggestions for a prefix /// Delete a string from a suggestion index.
/// </summary> /// </summary>
/// <param name="prefix">the prefix to complete on</param> /// <param name="value">the string to delete</param>
/// <param name="fuzzy"> if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent</param> public async Task<bool> DeleteSuggestionAsync(string value)
/// <param name="max">If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10).</param> => (long)await _db.ExecuteAsync("FT.SUGDEL", _boxedIndexName, value).ConfigureAwait(false) == 1;
/// <returns>a list of the top suggestions matching the prefix</returns>
public async Task<string[]> GetSuggestionsAsync(string prefix, bool fuzzy = false, int max = 5) /// <summary>
{ /// Get completion suggestions for a prefix
var args = new List<object> { _boxedIndexName, prefix }; /// </summary>
if (fuzzy) args.Add("FUZZY".Literal()); /// <param name="prefix">the prefix to complete on</param>
if (max != 5) /// <param name="fuzzy"> if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent</param>
{ /// <param name="max">If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10).</param>
args.Add("MAX".Literal()); /// <returns>a list of the top suggestions matching the prefix</returns>
args.Add(max); public string[] GetSuggestions(string prefix, bool fuzzy = false, int max = 5)
} {
return (string[])await _db.ExecuteAsync("FT.SUGGET", args).ConfigureAwait(false); var args = new List<object> { _boxedIndexName, prefix};
} if (fuzzy) args.Add("FUZZY".Literal());
} if (max != 5)
} {
args.Add("MAX".Literal());
args.Add(max);
}
return (string[])DbSync.Execute("FT.SUGGET", args);
}
/// <summary>
/// Get completion suggestions for a prefix
/// </summary>
/// <param name="prefix">the prefix to complete on</param>
/// <param name="fuzzy"> if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent</param>
/// <param name="max">If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10).</param>
/// <returns>a list of the top suggestions matching the prefix</returns>
public async Task<string[]> GetSuggestionsAsync(string prefix, bool fuzzy = false, int max = 5)
{
var args = new List<object> { _boxedIndexName, prefix };
if (fuzzy) args.Add("FUZZY".Literal());
if (max != 5)
{
args.Add("MAX".Literal());
args.Add(max);
}
return (string[])await _db.ExecuteAsync("FT.SUGGET", args).ConfigureAwait(false);
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/ // .NET port of https://github.com/RedisLabs/JRediSearch/
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace NRediSearch namespace NRediSearch
{ {
/// <summary> /// <summary>
/// Schema abstracts the schema definition when creating an index. /// Schema abstracts the schema definition when creating an index.
/// Documents can contain fields not mentioned in the schema, but the index will only index pre-defined fields /// Documents can contain fields not mentioned in the schema, but the index will only index pre-defined fields
/// </summary> /// </summary>
public sealed class Schema public sealed class Schema
{ {
public enum FieldType public enum FieldType
{ {
FullText, FullText,
Geo, Geo,
Numeric Numeric,
} Tag
}
public class Field
{ public class Field
public String Name { get; } {
public FieldType Type { get; } public String Name { get; }
public bool Sortable {get;} public FieldType Type { get; }
public bool Sortable {get;}
internal Field(string name, FieldType type, bool sortable)
{ internal Field(string name, FieldType type, bool sortable)
Name = name; {
Type = type; Name = name;
Sortable = sortable; Type = type;
} Sortable = sortable;
}
internal virtual void SerializeRedisArgs(List<object> args)
{ internal virtual void SerializeRedisArgs(List<object> args)
object GetForRedis(FieldType type) {
{ object GetForRedis(FieldType type)
switch (type) {
{ switch (type)
case FieldType.FullText: return "TEXT".Literal(); {
case FieldType.Geo: return "GEO".Literal(); case FieldType.FullText: return "TEXT".Literal();
case FieldType.Numeric: return "NUMERIC".Literal(); case FieldType.Geo: return "GEO".Literal();
default: throw new ArgumentOutOfRangeException(nameof(type)); case FieldType.Numeric: return "NUMERIC".Literal();
} case FieldType.Tag: return "TAG".Literal();
} default: throw new ArgumentOutOfRangeException(nameof(type));
args.Add(Name); }
args.Add(GetForRedis(Type)); }
if(Sortable){args.Add("SORTABLE");} args.Add(Name);
} args.Add(GetForRedis(Type));
} if(Sortable){args.Add("SORTABLE");}
public class TextField : Field }
{ }
public double Weight { get; } public class TextField : Field
internal TextField(string name, double weight = 1.0) : base(name, FieldType.FullText, false) {
{ public double Weight { get; }
Weight = weight; internal TextField(string name, double weight = 1.0) : base(name, FieldType.FullText, false)
} {
internal TextField(string name, bool sortable, double weight = 1.0) : base(name, FieldType.FullText, sortable) Weight = weight;
{ }
Weight = weight; internal TextField(string name, bool sortable, double weight = 1.0) : base(name, FieldType.FullText, sortable)
} {
internal override void SerializeRedisArgs(List<object> args) Weight = weight;
{ }
base.SerializeRedisArgs(args); internal override void SerializeRedisArgs(List<object> args)
if (Weight != 1.0) {
{ base.SerializeRedisArgs(args);
args.Add("WEIGHT".Literal()); if (Weight != 1.0)
args.Add(Weight); {
} args.Add("WEIGHT".Literal());
} args.Add(Weight);
} }
}
public List<Field> Fields { get; } = new List<Field>(); }
/// <summary> public List<Field> Fields { get; } = new List<Field>();
/// Add a text field to the schema with a given weight
/// </summary> /// <summary>
/// <param name="name">the field's name</param> /// Add a text field to the schema with a given weight
/// <param name="weight">its weight, a positive floating point number</param> /// </summary>
/// <returns>the schema object</returns> /// <param name="name">the field's name</param>
public Schema AddTextField(string name, double weight = 1.0) /// <param name="weight">its weight, a positive floating point number</param>
{ /// <returns>the schema object</returns>
Fields.Add(new TextField(name, weight)); public Schema AddTextField(string name, double weight = 1.0)
return this; {
} Fields.Add(new TextField(name, weight));
return this;
/// <summary> }
/// Add a text field that can be sorted on
/// </summary> /// <summary>
/// <param name="name">the field's name</param> /// Add a text field that can be sorted on
/// <param name="weight">its weight, a positive floating point number</param> /// </summary>
/// <returns>the schema object</returns> /// <param name="name">the field's name</param>
public Schema AddSortableTextField(string name, double weight = 1.0) /// <param name="weight">its weight, a positive floating point number</param>
{ /// <returns>the schema object</returns>
Fields.Add(new TextField(name, true, weight)); public Schema AddSortableTextField(string name, double weight = 1.0)
return this; {
} Fields.Add(new TextField(name, true, weight));
return this;
/// <summary> }
/// Add a numeric field to the schema
/// </summary> /// <summary>
/// <param name="name">the field's name</param> /// Add a numeric field to the schema
/// <returns>the schema object</returns> /// </summary>
public Schema AddGeoField(string name) /// <param name="name">the field's name</param>
{ /// <returns>the schema object</returns>
Fields.Add(new Field(name, FieldType.Geo, false)); public Schema AddGeoField(string name)
return this; {
} Fields.Add(new Field(name, FieldType.Geo, false));
return this;
/// <summary> }
/// Add a numeric field to the schema
/// </summary> /// <summary>
/// <param name="name">the field's name</param> /// Add a numeric field to the schema
/// <returns>the schema object</returns> /// </summary>
public Schema AddNumericField(string name) /// <param name="name">the field's name</param>
{ /// <returns>the schema object</returns>
Fields.Add(new Field(name, FieldType.Numeric, false)); public Schema AddNumericField(string name)
return this; {
} Fields.Add(new Field(name, FieldType.Numeric, false));
return this;
/// <summary> }
/// Add a numeric field that can be sorted on
/// </summary> /// <summary>
/// <param name="name">the field's name</param> /// Add a numeric field that can be sorted on
/// <returns>the schema object</returns> /// </summary>
public Schema AddSortableNumericField(string name) /// <param name="name">the field's name</param>
{ /// <returns>the schema object</returns>
Fields.Add(new Field(name, FieldType.Numeric, true)); public Schema AddSortableNumericField(string name)
return this; {
} Fields.Add(new Field(name, FieldType.Numeric, true));
return this;
} }
}
public class TagField : Field
{
public string Separator { get; }
internal TagField(string name, string separator = ",") : base(name, FieldType.Tag, false)
{
Separator = separator;
}
internal override void SerializeRedisArgs(List<object> args)
{
base.SerializeRedisArgs(args);
if (Separator != ",")
{
args.Add("SEPARATOR".Literal());
args.Add(Separator);
}
}
}
/// <summary>
/// Add a TAG field
/// </summary>
/// <param name="name">the field's name</param>
/// <param name="separator">tag separator</param>
/// <returns>the schema object</returns>
public Schema AddTagField(string name, string separator = ",")
{
Fields.Add(new TagField(name, separator));
return this;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment