From b571a7f6e03bfc0a6f7b73e6993e0d25e38ac99a Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Wed, 16 Oct 2024 18:38:27 -0700 Subject: [PATCH 01/18] Python: Add FT.SEARCH command Signed-off-by: Prateek Kumar --- glide-core/src/client/value_conversion.rs | 52 +++++- .../glide/async_commands/server_modules/ft.py | 42 ++++- .../server_modules/ft_options/ft_constants.py | 14 ++ .../ft_options/ft_search_options.py | 127 ++++++++++++++ .../search/test_ft_search.py | 161 ++++++++++++++++++ 5 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py create mode 100644 python/python/tests/tests_server_modules/search/test_ft_search.py diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 4a43da7da7..de1d798cea 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -1,7 +1,7 @@ /** * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -use redis::{ + use redis::{ cluster_routing::Routable, from_owned_redis_value, Cmd, ErrorKind, RedisResult, Value, }; @@ -22,6 +22,7 @@ pub(crate) enum ExpectedReturnType<'a> { ArrayOfStrings, ArrayOfBools, ArrayOfDoubleOrNull, + FTSearchReturnType, Lolwut, ArrayOfStringAndArrays, ArrayOfArraysOfDoubleOrNull, @@ -891,7 +892,53 @@ pub(crate) fn convert_to_expected_type( format!("(response was {:?})", get_value_type(&value)), ) .into()), - } + }, + ExpectedReturnType::FTSearchReturnType => match value { + /* + Example of the response + 1) (integer) 2 + 2) "json:2" + 3) 1) "__VEC_score" + 2) "11.1100006104" + 3) "$" + 4) "{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}" + 4) "json:0" + 5) 1) "__VEC_score" + 2) "91" + 3) "$" + 4) "{\"vec\":[1,2,3,4,5,6]}" + + Converting response to + 1) (integer) 2 + 2) 1# "json:2" => + 1# "__VEC_score" => "11.1100006104" + 2# "$" => "{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}" + 2# "json:0" => + 1# "__VEC_score" => "91" + 2# "$" => "{\"vec\":[1,2,3,4,5,6]}" + + Response may contain only 1 element, no conversion in that case. + */ + Value::Array(ref array) if array.len() == 1 => Ok(value), + Value::Array(mut array) => { + Ok(Value::Array(vec![ + array.remove(0), + convert_to_expected_type(Value::Array(array), Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::BulkString), + }), + }))? + ])) + }, + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to Pair", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()) + }, } } @@ -1256,6 +1303,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { key_type: &None, value_type: &None, }), + b"FT.SEARCH" => Some(ExpectedReturnType::FTSearchReturnType), _ => None, } } diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index 74d75e8953..25a26529ea 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -3,7 +3,7 @@ module for `vector search` commands. """ -from typing import List, Optional, cast +from typing import List, Mapping, Optional, Union, cast from glide.async_commands.server_modules.ft_options.ft_constants import ( CommandNames, @@ -13,6 +13,9 @@ Field, FtCreateOptions, ) +from glide.async_commands.server_modules.ft_options.ft_search_options import ( + FtSeachOptions, +) from glide.constants import TOK, TEncodable from glide.glide_client import TGlideClient @@ -76,3 +79,40 @@ async def dropindex(client: TGlideClient, indexName: TEncodable) -> TOK: """ args: List[TEncodable] = [CommandNames.FT_DROPINDEX, indexName] return cast(TOK, await client.custom_command(args)) + + +async def search( + client: TGlideClient, + indexName: TEncodable, + query: TEncodable, + options: Optional[FtSeachOptions], +) -> List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: + """ + Uses the provided query expression to locate keys within an index. + + Args: + client (TGlideClient): The client to execute the command. + indexName (TEncodable): The index name for the index to be searched. + query (TEncodable): The query expression to use for the search on the index. + options (Optional[FtSeachOptions]): Optional arguments for the FT.SEARCH command. See `FtSearchOptions`. + + Returns: + List[Union[int, Mapping[TEncodable, Mapping[TEncodable]]]]: A list containing the search result. The first element is the total number of keys matching the query. The second element is a map of key name and field/value pair map. + + Examples: + For the following example to work the following must already exist: + - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" + - A key named {json:}1 with value {"a": 1, "b":2} + + >>> from glide.async_commands.server_modules import ft + >>> index = "idx" + >>> result = await ft.search(glide_client, index, "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="a"),ReturnField(field_identifier="b")])) + [1, { b'{json:}1': {b'a': b'1', b'b' : b'2'}}] #The first element, 1 is the number of keys returned in the search result. The second element is field/value pair map for the index. + """ + args: List[TEncodable] = [CommandNames.FT_SEARCH, indexName, query] + if options: + args.extend(options.toArgs()) + return cast( + List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], + await client.custom_command(args), + ) diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py b/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py index d1e8e524eb..7347aa51e0 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py +++ b/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py @@ -8,6 +8,7 @@ class CommandNames: FT_CREATE = "FT.CREATE" FT_DROPINDEX = "FT.DROPINDEX" + FT_SEARCH = "FT.SEARCH" class FtCreateKeywords: @@ -31,3 +32,16 @@ class FtCreateKeywords: M = "M" EF_CONSTRUCTION = "EF_CONSTRUCTION" EF_RUNTIME = "EF_RUNTIME" + + +class FtSeachKeywords: + """ + Keywords used in the FT.SEARCH command statment. + """ + + RETURN = "RETURN" + TIMEOUT = "TIMEOUT" + PARAMS = "PARAMS" + LIMIT = "LIMIT" + COUNT = "COUNT" + AS = "AS" diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py b/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py new file mode 100644 index 0000000000..cfdbd837a4 --- /dev/null +++ b/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py @@ -0,0 +1,127 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +from typing import List, Mapping, Optional + +from glide.async_commands.server_modules.ft_options.ft_constants import FtSeachKeywords +from glide.constants import TEncodable + + +class Limit: + """ + This class represents the arguments for the LIMIT option of the FT.SEARCH command. + """ + + def __init__(self, offset: int, count: int): + """ + Initialize a new Limit instance. + + Args: + offset (int): The number of keys to skip before returning the result for the FT.SEARCH command. + count (int): The total number of keys to be returned by FT.SEARCH command. + """ + self.offset = offset + self.count = count + + def toArgs(self) -> List[TEncodable]: + """ + Get the arguments for the LIMIT option of FT.SEARCH. + + Returns: + List[TEncodable]: A list of LIMIT option arguments. + """ + args: List[TEncodable] = [ + FtSeachKeywords.LIMIT, + str(self.offset), + str(self.count), + ] + return args + + +class ReturnField: + """ + This class represents the arguments for the RETURN option of the FT.SEARCH command. + """ + + def __init__( + self, field_identifier: TEncodable, alias: Optional[TEncodable] = None + ): + """ + Initialize a new ReturnField instance. + + Args: + field_identifier (TEncodable): The identifier for the field of the key that has to returned as a result of FT.SEARCH command. + alias (Optional[TEncodable]): The alias to override the name of the field in the FT.SEARCH result. + """ + self.field_identifier = field_identifier + self.alias = alias + + def toArgs(self) -> List[TEncodable]: + """ + Get the arguments for the RETURN option of FT.SEARCH. + + Returns: + List[TEncodable]: A list of RETURN option arguments. + """ + args: List[TEncodable] = [self.field_identifier] + if self.alias: + args.append(FtSeachKeywords.AS) + args.append(self.alias) + return args + + +class FtSeachOptions: + """ + This class represents the input options to be used in the FT.SEARCH command. + All fields in this class are optional inputs for FT.SEARCH. + """ + + def __init__( + self, + return_fields: Optional[List[ReturnField]] = None, + timeout: Optional[int] = None, + params: Optional[Mapping[TEncodable, TEncodable]] = None, + limit: Optional[Limit] = None, + count: Optional[bool] = False, + ): + """ + Initialize the FT.SEARCH optional fields. + + Args: + return_fields (Optional[List[ReturnField]]): The fields of a key that are returned by FT.SEARCH command. See `ReturnField`. + timeout (Optional[int]): This value overrides the timeout parameter of the module. The unit for the timout is in milliseconds. + params (Optional[Mapping[TEncodable, TEncodable]]): Param key/value pairs that can be referenced from within the query expression. + limit (Optional[Limit]): This option provides pagination capability. Only the keys that satisfy the offset and count values are returned. See `Limit`. + count (Optional[bool]): This flag option suppresses returning the contents of keys. Only the number of keys is returned. + """ + self.return_fields = return_fields + self.timeout = timeout + self.params = params + self.limit = limit + self.count = count + + def toArgs(self) -> List[TEncodable]: + """ + Get the optional arguments for the FT.SEARCH command. + + Returns: + List[TEncodable]: + List of FT.SEARCH optional agruments. + """ + args: List[TEncodable] = [] + if self.return_fields: + args.append(FtSeachKeywords.RETURN) + return_field_args: List[TEncodable] = [] + for return_field in self.return_fields: + return_field_args.extend(return_field.toArgs()) + args.append(str(len(return_field_args))) + args.extend(return_field_args) + if self.timeout: + args.append(FtSeachKeywords.TIMEOUT) + args.append(str(self.timeout)) + if self.params: + args.append(FtSeachKeywords.PARAMS) + if self.limit: + args.extend(self.limit.toArgs()) + if self.count: + args.append(FtSeachKeywords.COUNT) + return args diff --git a/python/python/tests/tests_server_modules/search/test_ft_search.py b/python/python/tests/tests_server_modules/search/test_ft_search.py new file mode 100644 index 0000000000..8a5d810d97 --- /dev/null +++ b/python/python/tests/tests_server_modules/search/test_ft_search.py @@ -0,0 +1,161 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +import json as OuterJson +import time +import uuid +from typing import List, Mapping, Union, cast + +import pytest +from glide.async_commands.server_modules import ft, json +from glide.async_commands.server_modules.ft_options.ft_create_options import ( + DataType, + FtCreateOptions, + NumericField, +) +from glide.async_commands.server_modules.ft_options.ft_search_options import ( + FtSeachOptions, + ReturnField, +) +from glide.config import ProtocolVersion +from glide.constants import OK, TEncodable +from glide.glide_client import GlideClusterClient + + +@pytest.mark.asyncio +class TestFtSearch: + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_ft_search(self, glide_client: GlideClusterClient): + prefix = "{json-search-" + str(uuid.uuid4()) + "}:" + json_key1 = prefix + str(uuid.uuid4()) + json_key2 = prefix + str(uuid.uuid4()) + json_value1 = {"a": 11111, "b": 2, "c": 3} + json_value2 = {"a": 22222, "b": 2, "c": 3} + prefixes: List[TEncodable] = [] + prefixes.append(prefix) + index = "{json-search}:" + str(uuid.uuid4()) + + # Create an index + assert ( + await ft.create( + glide_client, + index, + schema=[ + NumericField("$.a", "a"), + NumericField("$.b", "b"), + ], + options=FtCreateOptions(DataType.JSON), + ) + == OK + ) + + # Create a json key + assert ( + await json.set(glide_client, json_key1, "$", OuterJson.dumps(json_value1)) + == OK + ) + assert ( + await json.set(glide_client, json_key2, "$", OuterJson.dumps(json_value2)) + == OK + ) + + # Wait for index to be updated to avoid this error - ResponseError: The index is under construction. + time.sleep(0.5) + + # Search the index for string inputs + result1 = await ft.search( + glide_client, + index, + "*", + options=FtSeachOptions( + return_fields=[ + ReturnField(field_identifier="a", alias="a_new"), + ReturnField(field_identifier="b", alias="b_new"), + ] + ), + ) + # Check if we get the expected result from ft.search for string inputs + TestFtSearch.f( + self, + result=result1, + json_key1=json_key1, + json_key2=json_key2, + json_value1=json_value1, + json_value2=json_value2, + ) + + # Search the index for byte inputs + result2 = await ft.search( + glide_client, + bytes(index, "utf-8"), + b"*", + options=FtSeachOptions( + return_fields=[ + ReturnField(field_identifier=b"a", alias=b"a_new"), + ReturnField(field_identifier=b"b", alias=b"b_new"), + ] + ), + ) + + # Check if we get the expected result from ft.search from byte inputs + TestFtSearch.f( + self, + result=result2, + json_key1=json_key1, + json_key2=json_key2, + json_value1=json_value1, + json_value2=json_value2, + ) + + def f( + self, + result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], + json_key1: str, + json_key2: str, + json_value1: dict, + json_value2: dict, + ): + type_name_bytes = "bytes" + assert len(result) == 2 + assert result[0] == 2 + searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( + Mapping[TEncodable, Mapping[TEncodable, TEncodable]], result[1] + ) + for key, fieldsMap in searchResultMap.items(): + keyString = key + if type(key).__name__ == type_name_bytes: + print(type(key).__name__) + keyString = cast(bytes, key).decode(encoding="utf-8") + assert keyString == json_key1 or keyString == json_key2 + if keyString == json_key1: + for fieldName, fieldValue in fieldsMap.items(): + fieldNameString = fieldName + if type(fieldName).__name__ == type_name_bytes: + fieldNameString = cast(bytes, fieldName).decode( + encoding="utf-8" + ) + fieldValueInt = int(fieldValue) + if type(fieldValue).__name__ == type_name_bytes: + fieldValueInt = int( + cast(bytes, fieldValue).decode(encoding="utf-8") + ) + assert fieldNameString == "a" or fieldNameString == "b" + assert fieldValueInt == json_value1.get( + "a" + ) or fieldValueInt == json_value1.get("b") + if keyString == json_key2: + for fieldName, fieldValue in fieldsMap.items(): + fieldNameString = fieldName + if type(fieldName).__name__ == type_name_bytes: + fieldNameString = cast(bytes, fieldName).decode( + encoding="utf-8" + ) + fieldValueInt = int(fieldValue) + if type(fieldValue).__name__ == type_name_bytes: + fieldValueInt = int( + cast(bytes, fieldValue).decode(encoding="utf-8") + ) + assert fieldNameString == "a" or fieldNameString == "b" + assert fieldValueInt == json_value2.get( + "a" + ) or fieldValueInt == json_value2.get("b") From dc8d2c49331d2b65a35e16415d8b6492faa3136d Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Thu, 17 Oct 2024 10:39:47 -0700 Subject: [PATCH 02/18] Python FT.SEARCH - rust file removed Signed-off-by: Prateek Kumar --- glide-core/src/client/value_conversion.rs | 50 +---------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index de1d798cea..fd9dd69863 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -22,7 +22,6 @@ pub(crate) enum ExpectedReturnType<'a> { ArrayOfStrings, ArrayOfBools, ArrayOfDoubleOrNull, - FTSearchReturnType, Lolwut, ArrayOfStringAndArrays, ArrayOfArraysOfDoubleOrNull, @@ -892,53 +891,7 @@ pub(crate) fn convert_to_expected_type( format!("(response was {:?})", get_value_type(&value)), ) .into()), - }, - ExpectedReturnType::FTSearchReturnType => match value { - /* - Example of the response - 1) (integer) 2 - 2) "json:2" - 3) 1) "__VEC_score" - 2) "11.1100006104" - 3) "$" - 4) "{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}" - 4) "json:0" - 5) 1) "__VEC_score" - 2) "91" - 3) "$" - 4) "{\"vec\":[1,2,3,4,5,6]}" - - Converting response to - 1) (integer) 2 - 2) 1# "json:2" => - 1# "__VEC_score" => "11.1100006104" - 2# "$" => "{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}" - 2# "json:0" => - 1# "__VEC_score" => "91" - 2# "$" => "{\"vec\":[1,2,3,4,5,6]}" - - Response may contain only 1 element, no conversion in that case. - */ - Value::Array(ref array) if array.len() == 1 => Ok(value), - Value::Array(mut array) => { - Ok(Value::Array(vec![ - array.remove(0), - convert_to_expected_type(Value::Array(array), Some(ExpectedReturnType::Map { - key_type: &Some(ExpectedReturnType::BulkString), - value_type: &Some(ExpectedReturnType::Map { - key_type: &Some(ExpectedReturnType::BulkString), - value_type: &Some(ExpectedReturnType::BulkString), - }), - }))? - ])) - }, - _ => Err(( - ErrorKind::TypeError, - "Response couldn't be converted to Pair", - format!("(response was {:?})", get_value_type(&value)), - ) - .into()) - }, + } } } @@ -1303,7 +1256,6 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { key_type: &None, value_type: &None, }), - b"FT.SEARCH" => Some(ExpectedReturnType::FTSearchReturnType), _ => None, } } From 560d1651aeef279ede08a62836d33fa4b26b0e6b Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Thu, 17 Oct 2024 10:41:21 -0700 Subject: [PATCH 03/18] Python FT.SEARCH Rust file removed Signed-off-by: Prateek Kumar --- glide-core/src/client/value_conversion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index fd9dd69863..4a43da7da7 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -1,7 +1,7 @@ /** * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ - use redis::{ +use redis::{ cluster_routing::Routable, from_owned_redis_value, Cmd, ErrorKind, RedisResult, Value, }; From e9377365d5dade51c73394969bd7836b0bbc6f9f Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Thu, 17 Oct 2024 12:11:07 -0700 Subject: [PATCH 04/18] Python FT.SEARCH review comments addressed Signed-off-by: Prateek Kumar --- .../glide/async_commands/server_modules/ft.py | 7 +++---- .../search/test_ft_search.py | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index 675e015b50..3260888d96 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -102,12 +102,11 @@ async def search( Examples: For the following example to work the following must already exist: - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" - - A key named {json:}1 with value {"a": 1, "b":2} + - A key named {json:}1 with value {"a":1, "b":2} >>> from glide.async_commands.server_modules import ft - >>> index = "idx" - >>> result = await ft.search(glide_client, index, "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="a"),ReturnField(field_identifier="b")])) - [1, { b'{json:}1': {b'a': b'1', b'b' : b'2'}}] #The first element, 1 is the number of keys returned in the search result. The second element is field/value pair map for the index. + >>> result = await ft.search(glide_client, "idx", "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="a"),ReturnField(field_identifier="b")])) + [1, { b'{json:}1': {b'a': b'1', b'b': b'2'}}] #The first element, 1 is the number of keys returned in the search result. The second element is field/value pair map for the index. """ args: List[TEncodable] = [CommandNames.FT_SEARCH, indexName, query] if options: diff --git a/python/python/tests/tests_server_modules/search/test_ft_search.py b/python/python/tests/tests_server_modules/search/test_ft_search.py index 8a5d810d97..1e740d7f45 100644 --- a/python/python/tests/tests_server_modules/search/test_ft_search.py +++ b/python/python/tests/tests_server_modules/search/test_ft_search.py @@ -1,12 +1,13 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -import json as OuterJson +import json import time import uuid from typing import List, Mapping, Union, cast import pytest -from glide.async_commands.server_modules import ft, json +from glide.async_commands.server_modules import ft +from glide.async_commands.server_modules import json as GlideJson from glide.async_commands.server_modules.ft_options.ft_create_options import ( DataType, FtCreateOptions, @@ -23,6 +24,8 @@ @pytest.mark.asyncio class TestFtSearch: + sleep_wait_time = 0.5 # T his value is in seconds + @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_ft_search(self, glide_client: GlideClusterClient): @@ -33,7 +36,7 @@ async def test_ft_search(self, glide_client: GlideClusterClient): json_value2 = {"a": 22222, "b": 2, "c": 3} prefixes: List[TEncodable] = [] prefixes.append(prefix) - index = "{json-search}:" + str(uuid.uuid4()) + index = prefix + str(uuid.uuid4()) # Create an index assert ( @@ -51,16 +54,16 @@ async def test_ft_search(self, glide_client: GlideClusterClient): # Create a json key assert ( - await json.set(glide_client, json_key1, "$", OuterJson.dumps(json_value1)) + await GlideJson.set(glide_client, json_key1, "$", json.dumps(json_value1)) == OK ) assert ( - await json.set(glide_client, json_key2, "$", OuterJson.dumps(json_value2)) + await GlideJson.set(glide_client, json_key2, "$", json.dumps(json_value2)) == OK ) # Wait for index to be updated to avoid this error - ResponseError: The index is under construction. - time.sleep(0.5) + time.sleep(self.sleep_wait_time) # Search the index for string inputs result1 = await ft.search( @@ -75,7 +78,7 @@ async def test_ft_search(self, glide_client: GlideClusterClient): ), ) # Check if we get the expected result from ft.search for string inputs - TestFtSearch.f( + TestFtSearch._ft_search_deep_compare_result( self, result=result1, json_key1=json_key1, @@ -98,7 +101,7 @@ async def test_ft_search(self, glide_client: GlideClusterClient): ) # Check if we get the expected result from ft.search from byte inputs - TestFtSearch.f( + TestFtSearch._ft_search_deep_compare_result( self, result=result2, json_key1=json_key1, @@ -107,7 +110,7 @@ async def test_ft_search(self, glide_client: GlideClusterClient): json_value2=json_value2, ) - def f( + def _ft_search_deep_compare_result( self, result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], json_key1: str, From 67680e9bffa43c105b487e9c464f99ad436b8b36 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Thu, 17 Oct 2024 14:17:41 -0700 Subject: [PATCH 05/18] Review comments fixed Signed-off-by: Prateek Kumar --- .../python/tests/tests_server_modules/search/test_ft_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/python/tests/tests_server_modules/search/test_ft_search.py b/python/python/tests/tests_server_modules/search/test_ft_search.py index 1e740d7f45..0a1b21c21b 100644 --- a/python/python/tests/tests_server_modules/search/test_ft_search.py +++ b/python/python/tests/tests_server_modules/search/test_ft_search.py @@ -24,7 +24,7 @@ @pytest.mark.asyncio class TestFtSearch: - sleep_wait_time = 0.5 # T his value is in seconds + sleep_wait_time = 0.5 # This value is in seconds @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) From 4d334f17e381fc23cc9a1a863ddfbae0bb9721f4 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Thu, 17 Oct 2024 17:05:33 -0700 Subject: [PATCH 06/18] Python: Review comments addressed Signed-off-by: Prateek Kumar --- .../glide/async_commands/server_modules/ft.py | 30 +++++++++---------- .../ft_options/ft_search_options.py | 4 +++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index 3260888d96..a47b6e33f6 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -88,25 +88,25 @@ async def search( options: Optional[FtSeachOptions], ) -> List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: """ - Uses the provided query expression to locate keys within an index. + Uses the provided query expression to locate keys within an index. - Args: - client (TGlideClient): The client to execute the command. - indexName (TEncodable): The index name for the index to be searched. - query (TEncodable): The query expression to use for the search on the index. - options (Optional[FtSeachOptions]): Optional arguments for the FT.SEARCH command. See `FtSearchOptions`. + Args: + client (TGlideClient): The client to execute the command. + indexName (TEncodable): The index name for the index to be searched. + query (TEncodable): The query expression to use for the search on the index. + options (Optional[FtSeachOptions]): Optional arguments for the FT.SEARCH command. See `FtSearchOptions`. - Returns: - List[Union[int, Mapping[TEncodable, Mapping[TEncodable]]]]: A list containing the search result. The first element is the total number of keys matching the query. The second element is a map of key name and field/value pair map. + Returns: + List[Union[int, Mapping[TEncodable, Mapping[TEncodable]]]]: A list containing the search result. The first element is the total number of keys matching the query. The second element is a map of key name and field/value pair map. - Examples: - For the following example to work the following must already exist: - - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" - - A key named {json:}1 with value {"a":1, "b":2} + Examples: + For the following example to work the following must already exist: + - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" + - A key named {json:}1 with value {"a":1, "b":2} - >>> from glide.async_commands.server_modules import ft - >>> result = await ft.search(glide_client, "idx", "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="a"),ReturnField(field_identifier="b")])) - [1, { b'{json:}1': {b'a': b'1', b'b': b'2'}}] #The first element, 1 is the number of keys returned in the search result. The second element is field/value pair map for the index. + >>> from glide.async_commands.server_modules import ft + >>> result = await ft.search(glide_client, "idx", "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="first"), ReturnField(field_identifier="second")])) + [1, { b'json:1': { b'first': b'42', b'second': b'33' } }] # The first element, 1 is the number of keys returned in the search result. The second element is a map of data queried per key. """ args: List[TEncodable] = [CommandNames.FT_SEARCH, indexName, query] if options: diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py b/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py index cfdbd837a4..e995098aa2 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py +++ b/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py @@ -120,6 +120,10 @@ def toArgs(self) -> List[TEncodable]: args.append(str(self.timeout)) if self.params: args.append(FtSeachKeywords.PARAMS) + args.append(str(len(self.params))) + for name, value in self.params.items(): + args.append(name) + args.append(value) if self.limit: args.extend(self.limit.toArgs()) if self.count: From 327faeacdbc2b2417e99c26f4ab79e787407d564 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Thu, 17 Oct 2024 17:10:23 -0700 Subject: [PATCH 07/18] CHANGELOG.md updated Signed-off-by: Prateek Kumar --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a90f628944..967909ad31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Python: Add FT.SEARCH command([#2470](https://github.com/valkey-io/valkey-glide/pull/2470)) * Python: Add commands FT.ALIASADD, FT.ALIASDEL, FT.ALIASUPDATE([#2471](https://github.com/valkey-io/valkey-glide/pull/2471)) * Python: Python FT.DROPINDEX command ([#2437](https://github.com/valkey-io/valkey-glide/pull/2437)) * Python: Python: Added FT.CREATE command([#2413](https://github.com/valkey-io/valkey-glide/pull/2413)) From 1869075a0b9890f4eba72d716f7e211ac6427f56 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 09:51:51 -0700 Subject: [PATCH 08/18] Python FT.SEARCH fix test Signed-off-by: Prateek Kumar --- .../search/test_ft_search.py | 62 ++----------------- python/python/tests/utils/utils.py | 52 ++++++++++++++++ 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/python/python/tests/tests_server_modules/search/test_ft_search.py b/python/python/tests/tests_server_modules/search/test_ft_search.py index 0a1b21c21b..7817a52ade 100644 --- a/python/python/tests/tests_server_modules/search/test_ft_search.py +++ b/python/python/tests/tests_server_modules/search/test_ft_search.py @@ -3,7 +3,7 @@ import json import time import uuid -from typing import List, Mapping, Union, cast +from typing import List import pytest from glide.async_commands.server_modules import ft @@ -20,6 +20,7 @@ from glide.config import ProtocolVersion from glide.constants import OK, TEncodable from glide.glide_client import GlideClusterClient +from tests.utils.utils import ft_search_deep_compare_result @pytest.mark.asyncio @@ -78,8 +79,7 @@ async def test_ft_search(self, glide_client: GlideClusterClient): ), ) # Check if we get the expected result from ft.search for string inputs - TestFtSearch._ft_search_deep_compare_result( - self, + ft_search_deep_compare_result( result=result1, json_key1=json_key1, json_key2=json_key2, @@ -101,64 +101,10 @@ async def test_ft_search(self, glide_client: GlideClusterClient): ) # Check if we get the expected result from ft.search from byte inputs - TestFtSearch._ft_search_deep_compare_result( - self, + ft_search_deep_compare_result( result=result2, json_key1=json_key1, json_key2=json_key2, json_value1=json_value1, json_value2=json_value2, ) - - def _ft_search_deep_compare_result( - self, - result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], - json_key1: str, - json_key2: str, - json_value1: dict, - json_value2: dict, - ): - type_name_bytes = "bytes" - assert len(result) == 2 - assert result[0] == 2 - searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( - Mapping[TEncodable, Mapping[TEncodable, TEncodable]], result[1] - ) - for key, fieldsMap in searchResultMap.items(): - keyString = key - if type(key).__name__ == type_name_bytes: - print(type(key).__name__) - keyString = cast(bytes, key).decode(encoding="utf-8") - assert keyString == json_key1 or keyString == json_key2 - if keyString == json_key1: - for fieldName, fieldValue in fieldsMap.items(): - fieldNameString = fieldName - if type(fieldName).__name__ == type_name_bytes: - fieldNameString = cast(bytes, fieldName).decode( - encoding="utf-8" - ) - fieldValueInt = int(fieldValue) - if type(fieldValue).__name__ == type_name_bytes: - fieldValueInt = int( - cast(bytes, fieldValue).decode(encoding="utf-8") - ) - assert fieldNameString == "a" or fieldNameString == "b" - assert fieldValueInt == json_value1.get( - "a" - ) or fieldValueInt == json_value1.get("b") - if keyString == json_key2: - for fieldName, fieldValue in fieldsMap.items(): - fieldNameString = fieldName - if type(fieldName).__name__ == type_name_bytes: - fieldNameString = cast(bytes, fieldName).decode( - encoding="utf-8" - ) - fieldValueInt = int(fieldValue) - if type(fieldValue).__name__ == type_name_bytes: - fieldValueInt = int( - cast(bytes, fieldValue).decode(encoding="utf-8") - ) - assert fieldNameString == "a" or fieldNameString == "b" - assert fieldValueInt == json_value2.get( - "a" - ) or fieldValueInt == json_value2.get("b") diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index 497342b5c7..932599adca 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -6,7 +6,9 @@ import pytest from glide.async_commands.core import InfoSection from glide.constants import ( + OK, TClusterResponse, + TEncodable, TFunctionListResponse, TFunctionStatsSingleNodeResponse, TResult, @@ -359,3 +361,53 @@ def check_function_stats_response( b"LUA": {b"libraries_count": lib_count, b"functions_count": function_count} } assert expected == response.get(b"engines") + + +def ft_search_deep_compare_result( + result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], + json_key1: str, + json_key2: str, + json_value1: dict, + json_value2: dict, +): + + assert len(result) == 2 + assert result[0] == 2 + searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( + Mapping[TEncodable, Mapping[TEncodable, TEncodable]], result[1] + ) + for key, fieldsMap in searchResultMap.items(): + keyString = convert_bytes_to_string(key) + assert keyString == json_key1 or keyString == json_key2 + if keyString == json_key1: + for fieldName, fieldValue in fieldsMap.items(): + fieldNameString = convert_bytes_to_string(fieldName) + fieldValueString = convert_bytes_to_string(fieldValue) + fieldValueInt = convert_str_to_int(fieldValueString) + + assert fieldNameString == "a" or fieldNameString == "b" + assert fieldValueInt == json_value1.get( + "a" + ) or fieldValueInt == json_value1.get("b") + + if keyString == json_key2: + for fieldName, fieldValue in fieldsMap.items(): + fieldNameString = convert_bytes_to_string(fieldName) + fieldValueString = convert_bytes_to_string(fieldValue) + fieldValueInt = convert_str_to_int(fieldValueString) + assert fieldNameString == "a" or fieldNameString == "b" + assert fieldValueInt == json_value2.get( + "a" + ) or fieldValueInt == json_value2.get("b") + + +def convert_bytes_to_string(field: Union[str, bytes]) -> str: + type_name_bytes = "bytes" + fieldNameString = field + if type(field).__name__ == type_name_bytes: + fieldNameString = cast(bytes, field).decode(encoding="utf-8") + return str(fieldNameString) + + +def convert_str_to_int(field: str) -> int: + return int(field) From dfe1d36ef81d0bdd5dfdaaa8df6b4d5b0317ea78 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 10:08:47 -0700 Subject: [PATCH 09/18] Python FT.SEARCH add documentation to utils Signed-off-by: Prateek Kumar --- python/python/tests/utils/utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index 932599adca..a57d60c638 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -370,7 +370,16 @@ def ft_search_deep_compare_result( json_value1: dict, json_value2: dict, ): + """ + Deep compare the keys and values in FT.SEARCH result array. + Args: + result (List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]): + json_key1 (str): The first key in search result. + json_key2 (str): The second key in the search result. + json_value1 (dict): The fields map for first key in the search result. + json_value2 (dict): The fields map for second key in the search result. + """ assert len(result) == 2 assert result[0] == 2 searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( @@ -402,6 +411,16 @@ def ft_search_deep_compare_result( def convert_bytes_to_string(field: Union[str, bytes]) -> str: + """ + Convert Union[str, bytes] type variable to str. + + Args: + field (Union[str, bytes]): Field to be converted to str type. + + Returns: + str: + Field converted to str type. + """ type_name_bytes = "bytes" fieldNameString = field if type(field).__name__ == type_name_bytes: @@ -410,4 +429,14 @@ def convert_bytes_to_string(field: Union[str, bytes]) -> str: def convert_str_to_int(field: str) -> int: + """ + Convert str type variable to int. + + Args: + field (str): Field to be converted to int type. + + Returns: + int: + Field converted to int type. + """ return int(field) From 9587957e70ffd63e237b3c820285d3fba1cd7134 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 14:44:53 -0700 Subject: [PATCH 10/18] Python FT.SEARCH test case updated Signed-off-by: Prateek Kumar --- python/python/tests/utils/utils.py | 35 ++++++++++-------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index a57d60c638..d124686f63 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -385,29 +385,18 @@ def ft_search_deep_compare_result( searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( Mapping[TEncodable, Mapping[TEncodable, TEncodable]], result[1] ) - for key, fieldsMap in searchResultMap.items(): - keyString = convert_bytes_to_string(key) - assert keyString == json_key1 or keyString == json_key2 - if keyString == json_key1: - for fieldName, fieldValue in fieldsMap.items(): - fieldNameString = convert_bytes_to_string(fieldName) - fieldValueString = convert_bytes_to_string(fieldValue) - fieldValueInt = convert_str_to_int(fieldValueString) - - assert fieldNameString == "a" or fieldNameString == "b" - assert fieldValueInt == json_value1.get( - "a" - ) or fieldValueInt == json_value1.get("b") - - if keyString == json_key2: - for fieldName, fieldValue in fieldsMap.items(): - fieldNameString = convert_bytes_to_string(fieldName) - fieldValueString = convert_bytes_to_string(fieldValue) - fieldValueInt = convert_str_to_int(fieldValueString) - assert fieldNameString == "a" or fieldNameString == "b" - assert fieldValueInt == json_value2.get( - "a" - ) or fieldValueInt == json_value2.get("b") + fieldName1 = "a" + fieldName2 = "b" + assert searchResultMap == { + json_key1.encode(): { + fieldName1.encode(): str(json_value1.get(fieldName1)).encode(), + fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), + }, + json_key2.encode(): { + fieldName1.encode(): str(json_value1.get(fieldName1)).encode(), + fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), + }, + } def convert_bytes_to_string(field: Union[str, bytes]) -> str: From 8772b610f034ede60dbc22cbb50c72b1b9bef5fa Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 14:57:19 -0700 Subject: [PATCH 11/18] Python test case updated Signed-off-by: Prateek Kumar --- python/python/tests/utils/utils.py | 41 +++++------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index d124686f63..dea6bc47b8 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -387,45 +387,16 @@ def ft_search_deep_compare_result( ) fieldName1 = "a" fieldName2 = "b" - assert searchResultMap == { + expectedResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = { json_key1.encode(): { fieldName1.encode(): str(json_value1.get(fieldName1)).encode(), - fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), + fieldName2.encode(): str(json_value1.get(fieldName2)).encode(), }, json_key2.encode(): { - fieldName1.encode(): str(json_value1.get(fieldName1)).encode(), + fieldName1.encode(): str(json_value2.get(fieldName1)).encode(), fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), }, } - - -def convert_bytes_to_string(field: Union[str, bytes]) -> str: - """ - Convert Union[str, bytes] type variable to str. - - Args: - field (Union[str, bytes]): Field to be converted to str type. - - Returns: - str: - Field converted to str type. - """ - type_name_bytes = "bytes" - fieldNameString = field - if type(field).__name__ == type_name_bytes: - fieldNameString = cast(bytes, field).decode(encoding="utf-8") - return str(fieldNameString) - - -def convert_str_to_int(field: str) -> int: - """ - Convert str type variable to int. - - Args: - field (str): Field to be converted to int type. - - Returns: - int: - Field converted to int type. - """ - return int(field) + print(expectedResultMap) + assert True == False + assert searchResultMap == expectedResultMap From 30a3fa30d0e0949868b14a4a6c624b57d65cc1e0 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 15:03:18 -0700 Subject: [PATCH 12/18] Python FT.SEARCH test case updated Signed-off-by: Prateek Kumar --- python/python/tests/utils/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index dea6bc47b8..c60278fac9 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -397,6 +397,4 @@ def ft_search_deep_compare_result( fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), }, } - print(expectedResultMap) - assert True == False assert searchResultMap == expectedResultMap From c137a27d78c76f2e3e7ad23f91f9a9a26dc5c71b Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 16:20:19 -0700 Subject: [PATCH 13/18] Python FT.SEARCH fix review comments Signed-off-by: Prateek Kumar --- python/python/glide/__init__.py | 8 +++ .../glide/async_commands/server_modules/ft.py | 30 +++++------ .../ft_options/ft_search_options.py | 8 +-- .../search/test_ft_search.py | 50 +++++++++++++++++-- python/python/tests/utils/utils.py | 38 -------------- 5 files changed, 73 insertions(+), 61 deletions(-) diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index 05910eb480..5bb31c75fb 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -49,6 +49,11 @@ VectorFieldAttributesHnsw, VectorType, ) +from glide.async_commands.server_modules.ft_options.ft_search_options import ( + FtSeachOptions, + FtSearchLimit, + ReturnField, +) from glide.async_commands.sorted_set import ( AggregationType, GeoSearchByBox, @@ -265,4 +270,7 @@ "VectorFieldAttributesFlat", "VectorFieldAttributesHnsw", "VectorType", + "FtSearchLimit", + "ReturnField", + "FtSeachOptions", ] diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index b7203760bc..110ec38b2f 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -88,25 +88,25 @@ async def search( options: Optional[FtSeachOptions], ) -> List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: """ - Uses the provided query expression to locate keys within an index. + Uses the provided query expression to locate keys within an index. - Args: - client (TGlideClient): The client to execute the command. - indexName (TEncodable): The index name for the index to be searched. - query (TEncodable): The query expression to use for the search on the index. - options (Optional[FtSeachOptions]): Optional arguments for the FT.SEARCH command. See `FtSearchOptions`. + Args: + client (TGlideClient): The client to execute the command. + indexName (TEncodable): The index name for the index to be searched. + query (TEncodable): The query expression to use for the search on the index. + options (Optional[FtSeachOptions]): Optional arguments for the FT.SEARCH command. See `FtSearchOptions`. - Returns: - List[Union[int, Mapping[TEncodable, Mapping[TEncodable]]]]: A list containing the search result. The first element is the total number of keys matching the query. The second element is a map of key name and field/value pair map. + Returns: + List[Union[int, Mapping[TEncodable, Mapping[TEncodable]]]]: A list containing the search result. The first element is the total number of keys matching the query. The second element is a map of key name and field/value pair map. - Examples: - For the following example to work the following must already exist: - - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" - - A key named {json:}1 with value {"a":1, "b":2} + Examples: + For the following example to work the following must already exist: + - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" + - A key named {json:}1 with value {"a":1, "b":2} - >>> from glide.async_commands.server_modules import ft - >>> result = await ft.search(glide_client, "idx", "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="first"), ReturnField(field_identifier="second")])) - [1, { b'json:1': { b'first': b'42', b'second': b'33' } }] # The first element, 1 is the number of keys returned in the search result. The second element is a map of data queried per key. + >>> from glide.async_commands.server_modules import ft + >>> result = await ft.search(glide_client, "idx", "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="first"), ReturnField(field_identifier="second")])) + [1, { b'json:1': { b'first': b'42', b'second': b'33' } }] # The first element, 1 is the number of keys returned in the search result. The second element is a map of data queried per key. """ args: List[TEncodable] = [CommandNames.FT_SEARCH, indexName, query] if options: diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py b/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py index e995098aa2..79f5422edc 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py +++ b/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py @@ -6,14 +6,14 @@ from glide.constants import TEncodable -class Limit: +class FtSearchLimit: """ This class represents the arguments for the LIMIT option of the FT.SEARCH command. """ def __init__(self, offset: int, count: int): """ - Initialize a new Limit instance. + Initialize a new FtSearchLimit instance. Args: offset (int): The number of keys to skip before returning the result for the FT.SEARCH command. @@ -80,7 +80,7 @@ def __init__( return_fields: Optional[List[ReturnField]] = None, timeout: Optional[int] = None, params: Optional[Mapping[TEncodable, TEncodable]] = None, - limit: Optional[Limit] = None, + limit: Optional[FtSearchLimit] = None, count: Optional[bool] = False, ): """ @@ -90,7 +90,7 @@ def __init__( return_fields (Optional[List[ReturnField]]): The fields of a key that are returned by FT.SEARCH command. See `ReturnField`. timeout (Optional[int]): This value overrides the timeout parameter of the module. The unit for the timout is in milliseconds. params (Optional[Mapping[TEncodable, TEncodable]]): Param key/value pairs that can be referenced from within the query expression. - limit (Optional[Limit]): This option provides pagination capability. Only the keys that satisfy the offset and count values are returned. See `Limit`. + limit (Optional[FtSearchLimit]): This option provides pagination capability. Only the keys that satisfy the offset and count values are returned. See `FtSearchLimit`. count (Optional[bool]): This flag option suppresses returning the contents of keys. Only the number of keys is returned. """ self.return_fields = return_fields diff --git a/python/python/tests/tests_server_modules/search/test_ft_search.py b/python/python/tests/tests_server_modules/search/test_ft_search.py index 7817a52ade..80c5e3ef82 100644 --- a/python/python/tests/tests_server_modules/search/test_ft_search.py +++ b/python/python/tests/tests_server_modules/search/test_ft_search.py @@ -3,7 +3,7 @@ import json import time import uuid -from typing import List +from typing import List, Mapping, Union, cast import pytest from glide.async_commands.server_modules import ft @@ -20,7 +20,6 @@ from glide.config import ProtocolVersion from glide.constants import OK, TEncodable from glide.glide_client import GlideClusterClient -from tests.utils.utils import ft_search_deep_compare_result @pytest.mark.asyncio @@ -79,12 +78,15 @@ async def test_ft_search(self, glide_client: GlideClusterClient): ), ) # Check if we get the expected result from ft.search for string inputs - ft_search_deep_compare_result( + TestFtSearch._ft_search_deep_compare_result( + self, result=result1, json_key1=json_key1, json_key2=json_key2, json_value1=json_value1, json_value2=json_value2, + fieldName1="a", + fieldName2="b", ) # Search the index for byte inputs @@ -101,10 +103,50 @@ async def test_ft_search(self, glide_client: GlideClusterClient): ) # Check if we get the expected result from ft.search from byte inputs - ft_search_deep_compare_result( + TestFtSearch._ft_search_deep_compare_result( + self, result=result2, json_key1=json_key1, json_key2=json_key2, json_value1=json_value1, json_value2=json_value2, + fieldName1="a", + fieldName2="b", ) + + def _ft_search_deep_compare_result( + self, + result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], + json_key1: str, + json_key2: str, + json_value1: dict, + json_value2: dict, + fieldName1: str, + fieldName2: str, + ): + """ + Deep compare the keys and values in FT.SEARCH result array. + + Args: + result (List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]): + json_key1 (str): The first key in search result. + json_key2 (str): The second key in the search result. + json_value1 (dict): The fields map for first key in the search result. + json_value2 (dict): The fields map for second key in the search result. + """ + assert len(result) == 2 + assert result[0] == 2 + searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( + Mapping[TEncodable, Mapping[TEncodable, TEncodable]], result[1] + ) + expectedResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = { + json_key1.encode(): { + fieldName1.encode(): str(json_value1.get(fieldName1)).encode(), + fieldName2.encode(): str(json_value1.get(fieldName2)).encode(), + }, + json_key2.encode(): { + fieldName1.encode(): str(json_value2.get(fieldName1)).encode(), + fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), + }, + } + assert searchResultMap == expectedResultMap diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index c60278fac9..c194be73e0 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -8,7 +8,6 @@ from glide.constants import ( OK, TClusterResponse, - TEncodable, TFunctionListResponse, TFunctionStatsSingleNodeResponse, TResult, @@ -361,40 +360,3 @@ def check_function_stats_response( b"LUA": {b"libraries_count": lib_count, b"functions_count": function_count} } assert expected == response.get(b"engines") - - -def ft_search_deep_compare_result( - result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], - json_key1: str, - json_key2: str, - json_value1: dict, - json_value2: dict, -): - """ - Deep compare the keys and values in FT.SEARCH result array. - - Args: - result (List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]): - json_key1 (str): The first key in search result. - json_key2 (str): The second key in the search result. - json_value1 (dict): The fields map for first key in the search result. - json_value2 (dict): The fields map for second key in the search result. - """ - assert len(result) == 2 - assert result[0] == 2 - searchResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = cast( - Mapping[TEncodable, Mapping[TEncodable, TEncodable]], result[1] - ) - fieldName1 = "a" - fieldName2 = "b" - expectedResultMap: Mapping[TEncodable, Mapping[TEncodable, TEncodable]] = { - json_key1.encode(): { - fieldName1.encode(): str(json_value1.get(fieldName1)).encode(), - fieldName2.encode(): str(json_value1.get(fieldName2)).encode(), - }, - json_key2.encode(): { - fieldName1.encode(): str(json_value2.get(fieldName1)).encode(), - fieldName2.encode(): str(json_value2.get(fieldName2)).encode(), - }, - } - assert searchResultMap == expectedResultMap From c666d074bd7c03936478499cfc8bff2fcf112c91 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Fri, 18 Oct 2024 16:57:00 -0700 Subject: [PATCH 14/18] Python FT.SEARCH utils updated Signed-off-by: Prateek Kumar --- python/python/tests/utils/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index c194be73e0..497342b5c7 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -6,7 +6,6 @@ import pytest from glide.async_commands.core import InfoSection from glide.constants import ( - OK, TClusterResponse, TFunctionListResponse, TFunctionStatsSingleNodeResponse, From b06c1fe3b8343d88628b0d2d9f1e21569b99e9a2 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Mon, 21 Oct 2024 17:49:44 -0700 Subject: [PATCH 15/18] Python FT.SEARCH delete index after ft.search test Signed-off-by: Prateek Kumar --- .../python/tests/tests_server_modules/search/test_ft_search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/python/tests/tests_server_modules/search/test_ft_search.py b/python/python/tests/tests_server_modules/search/test_ft_search.py index 80c5e3ef82..80d8319676 100644 --- a/python/python/tests/tests_server_modules/search/test_ft_search.py +++ b/python/python/tests/tests_server_modules/search/test_ft_search.py @@ -114,6 +114,8 @@ async def test_ft_search(self, glide_client: GlideClusterClient): fieldName2="b", ) + assert await ft.dropindex(glide_client, indexName=index) == OK + def _ft_search_deep_compare_result( self, result: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]], From ca5f9f155a2e5eb3cff87842775ec7015428af08 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Mon, 21 Oct 2024 18:34:11 -0700 Subject: [PATCH 16/18] Python: update documentation Signed-off-by: Prateek Kumar --- .../glide/async_commands/server_modules/ft.py | 14 +++++++------- .../server_modules/ft_options/ft_constants.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index 110ec38b2f..fce97d4cce 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -88,17 +88,17 @@ async def search( options: Optional[FtSeachOptions], ) -> List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: """ - Uses the provided query expression to locate keys within an index. + Uses the provided query expression to locate keys within an index. Once located, the count and/or the content of indexed fields within those keys can be returned. Args: client (TGlideClient): The client to execute the command. - indexName (TEncodable): The index name for the index to be searched. - query (TEncodable): The query expression to use for the search on the index. - options (Optional[FtSeachOptions]): Optional arguments for the FT.SEARCH command. See `FtSearchOptions`. + indexName (TEncodable): The index name to search into. + query (TEncodable): The text query to search. + options (Optional[FtSeachOptions]): The search options. See `FtSearchOptions`. Returns: - List[Union[int, Mapping[TEncodable, Mapping[TEncodable]]]]: A list containing the search result. The first element is the total number of keys matching the query. The second element is a map of key name and field/value pair map. - + List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: A two element array, where first element is count of documents in result set, and the second element, which has the format Mapping[TEncodable, Mapping[TEncodable, TEncodable]] is a mapping between document names and map of their attributes. + If count(option in `FtSearchOptions`) is set to true is set or limit(option in `FtSearchOptions`) is set to FtSearchLimit(0, 0) is set, the command returns array with only one element - the count of the documents. See `FtSearchOptions` and see `FtSearchLimit`. Examples: For the following example to work the following must already exist: - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" @@ -106,7 +106,7 @@ async def search( >>> from glide.async_commands.server_modules import ft >>> result = await ft.search(glide_client, "idx", "*", options=FtSeachOptions(return_fields=[ReturnField(field_identifier="first"), ReturnField(field_identifier="second")])) - [1, { b'json:1': { b'first': b'42', b'second': b'33' } }] # The first element, 1 is the number of keys returned in the search result. The second element is a map of data queried per key. + [1, { b'json:1': { b'first': b'42', b'second': b'33' } }] # The first element, 1 is the number of keys returned in the search result. The second element is a map of data queried per key. """ args: List[TEncodable] = [CommandNames.FT_SEARCH, indexName, query] if options: diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py b/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py index 19e4a38478..541b286d83 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py +++ b/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py @@ -16,7 +16,7 @@ class CommandNames: class FtCreateKeywords: """ - Keywords used in the FT.CREATE command statment. + Keywords used in the FT.CREATE command. """ SCHEMA = "SCHEMA" @@ -39,7 +39,7 @@ class FtCreateKeywords: class FtSeachKeywords: """ - Keywords used in the FT.SEARCH command statment. + Keywords used in the FT.SEARCH command. """ RETURN = "RETURN" From bb1b65d1dac54f867c05e19aefc1a7b8dde03a4a Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Mon, 21 Oct 2024 18:36:18 -0700 Subject: [PATCH 17/18] Python FT.SEARCH fix documenation Signed-off-by: Prateek Kumar --- python/python/glide/async_commands/server_modules/ft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index fce97d4cce..6dc0e2b3cb 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -98,7 +98,7 @@ async def search( Returns: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: A two element array, where first element is count of documents in result set, and the second element, which has the format Mapping[TEncodable, Mapping[TEncodable, TEncodable]] is a mapping between document names and map of their attributes. - If count(option in `FtSearchOptions`) is set to true is set or limit(option in `FtSearchOptions`) is set to FtSearchLimit(0, 0) is set, the command returns array with only one element - the count of the documents. See `FtSearchOptions` and see `FtSearchLimit`. + If count(option in `FtSearchOptions`) is set to true or limit(option in `FtSearchOptions`) is set to FtSearchLimit(0, 0), the command returns array with only one element - the count of the documents. See `FtSearchOptions` and see `FtSearchLimit`. Examples: For the following example to work the following must already exist: - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" From ce312a54a6a6582a6be2433a4361859bfa103100 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Mon, 21 Oct 2024 18:40:54 -0700 Subject: [PATCH 18/18] Python FT.SEARCH fix documentation Signed-off-by: Prateek Kumar --- python/python/glide/async_commands/server_modules/ft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/python/glide/async_commands/server_modules/ft.py index 6dc0e2b3cb..82118e9070 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/python/glide/async_commands/server_modules/ft.py @@ -98,7 +98,7 @@ async def search( Returns: List[Union[int, Mapping[TEncodable, Mapping[TEncodable, TEncodable]]]]: A two element array, where first element is count of documents in result set, and the second element, which has the format Mapping[TEncodable, Mapping[TEncodable, TEncodable]] is a mapping between document names and map of their attributes. - If count(option in `FtSearchOptions`) is set to true or limit(option in `FtSearchOptions`) is set to FtSearchLimit(0, 0), the command returns array with only one element - the count of the documents. See `FtSearchOptions` and see `FtSearchLimit`. + If count(option in `FtSearchOptions`) is set to true or limit(option in `FtSearchOptions`) is set to FtSearchLimit(0, 0), the command returns array with only one element - the count of the documents. Examples: For the following example to work the following must already exist: - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}"