algolia 2.0.2 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +10 -1
- data/.dockerignore +38 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +36 -6
- data/CONTRIBUTING.MD +184 -0
- data/DOCKER_README.MD +89 -0
- data/Dockerfile +7 -0
- data/README.md +1 -1
- data/lib/algolia/config/recommend_config.rb +6 -0
- data/lib/algolia/helpers.rb +49 -0
- data/lib/algolia/http/http_requester.rb +4 -4
- data/lib/algolia/recommend_client.rb +134 -0
- data/lib/algolia/responses/dictionary_response.rb +33 -0
- data/lib/algolia/search_client.rb +177 -3
- data/lib/algolia/search_index.rb +9 -50
- data/lib/algolia/version.rb +1 -1
- data/lib/algolia.rb +3 -0
- data/test/algolia/integration/analytics_client_test.rb +6 -2
- data/test/algolia/integration/mocks/mock_requester.rb +13 -11
- data/test/algolia/integration/recommend_client_test.rb +70 -0
- data/test/algolia/integration/search_client_test.rb +107 -12
- data/test/algolia/integration/search_index_test.rb +3 -0
- data/test/test_helper.rb +28 -0
- metadata +12 -3
@@ -0,0 +1,33 @@
|
|
1
|
+
module Algolia
|
2
|
+
class DictionaryResponse < BaseResponse
|
3
|
+
include CallType
|
4
|
+
|
5
|
+
attr_reader :raw_response
|
6
|
+
|
7
|
+
# @param client [Search::Client] Algolia Search Client used for verification
|
8
|
+
# @param response [Hash] Raw response from the client
|
9
|
+
#
|
10
|
+
def initialize(client, response)
|
11
|
+
@client = client
|
12
|
+
@raw_response = response
|
13
|
+
@done = false
|
14
|
+
end
|
15
|
+
|
16
|
+
# Wait for the task to complete
|
17
|
+
#
|
18
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
19
|
+
#
|
20
|
+
def wait(_opts = {})
|
21
|
+
until @done
|
22
|
+
res = @client.custom_request({}, path_encode('/1/task/%s', @raw_response[:taskID]), :GET, READ)
|
23
|
+
status = get_option(res, 'status')
|
24
|
+
if status == 'published'
|
25
|
+
@done = true
|
26
|
+
end
|
27
|
+
sleep(Defaults::WAIT_TASK_DEFAULT_TIME_BEFORE_RETRY / 1000)
|
28
|
+
end
|
29
|
+
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -80,7 +80,7 @@ module Algolia
|
|
80
80
|
end
|
81
81
|
|
82
82
|
# # # # # # # # # # # # # # # # # # # # #
|
83
|
-
#
|
83
|
+
# INDEX METHODS
|
84
84
|
# # # # # # # # # # # # # # # # # # # # #
|
85
85
|
|
86
86
|
# Initialize an index with a given name
|
@@ -300,7 +300,7 @@ module Algolia
|
|
300
300
|
# @return [AddApiKeyResponse]
|
301
301
|
#
|
302
302
|
def add_api_key!(acl, opts = {})
|
303
|
-
response = add_api_key(acl)
|
303
|
+
response = add_api_key(acl, opts)
|
304
304
|
|
305
305
|
response.wait(opts)
|
306
306
|
end
|
@@ -480,6 +480,7 @@ module Algolia
|
|
480
480
|
def multiple_queries(queries, opts = {})
|
481
481
|
@transporter.read(:POST, '/1/indexes/*/queries', { requests: queries }, opts)
|
482
482
|
end
|
483
|
+
alias_method :search, :multiple_queries
|
483
484
|
|
484
485
|
# # # # # # # # # # # # # # # # # # # # #
|
485
486
|
# MCM METHODS
|
@@ -594,12 +595,185 @@ module Algolia
|
|
594
595
|
@transporter.read(:GET, '/1/clusters/mapping/pending' + handle_params({ getClusters: retrieve_mappings }), {}, request_options)
|
595
596
|
end
|
596
597
|
|
597
|
-
#
|
598
598
|
# Aliases the pending_mappings? method
|
599
599
|
#
|
600
600
|
alias_method :has_pending_mappings, :pending_mappings?
|
601
601
|
|
602
|
+
# # # # # # # # # # # # # # # # # # # # #
|
603
|
+
# CUSTOM DICTIONARIES METHODS
|
604
|
+
# # # # # # # # # # # # # # # # # # # # #
|
605
|
+
|
606
|
+
# Save entries for a given dictionary
|
607
|
+
#
|
608
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
609
|
+
# @param dictionary_entries [Array<Hash>] array of dictionary entries
|
610
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
611
|
+
#
|
612
|
+
# @return DictionaryResponse
|
613
|
+
#
|
614
|
+
def save_dictionary_entries(dictionary, dictionary_entries, opts = {})
|
615
|
+
response = @transporter.write(
|
616
|
+
:POST,
|
617
|
+
path_encode('/1/dictionaries/%s/batch', dictionary),
|
618
|
+
{ clearExistingDictionaryEntries: false, requests: chunk('addEntry', dictionary_entries) },
|
619
|
+
opts
|
620
|
+
)
|
621
|
+
|
622
|
+
DictionaryResponse.new(self, response)
|
623
|
+
end
|
624
|
+
|
625
|
+
# Save entries for a given dictionary and wait for the task to finish
|
626
|
+
#
|
627
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
628
|
+
# @param dictionary_entries [Array<Hash>] array of dictionary entries
|
629
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
630
|
+
#
|
631
|
+
def save_dictionary_entries!(dictionary, dictionary_entries, opts = {})
|
632
|
+
response = save_dictionary_entries(dictionary, dictionary_entries, opts)
|
633
|
+
|
634
|
+
response.wait(opts)
|
635
|
+
end
|
636
|
+
|
637
|
+
# Replace entries for a given dictionary
|
638
|
+
#
|
639
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
640
|
+
# @param dictionary_entries [Array<Hash>] array of dictionary entries
|
641
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
642
|
+
#
|
643
|
+
# @return DictionaryResponse
|
644
|
+
#
|
645
|
+
def replace_dictionary_entries(dictionary, dictionary_entries, opts = {})
|
646
|
+
response = @transporter.write(
|
647
|
+
:POST,
|
648
|
+
path_encode('/1/dictionaries/%s/batch', dictionary),
|
649
|
+
{ clearExistingDictionaryEntries: true, requests: chunk('addEntry', dictionary_entries) },
|
650
|
+
opts
|
651
|
+
)
|
652
|
+
|
653
|
+
DictionaryResponse.new(self, response)
|
654
|
+
end
|
655
|
+
|
656
|
+
# Replace entries for a given dictionary and wait for the task to finish
|
657
|
+
#
|
658
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
659
|
+
# @param dictionary_entries [Array<Hash>] array of dictionary entries
|
660
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
661
|
+
#
|
662
|
+
def replace_dictionary_entries!(dictionary, dictionary_entries, opts = {})
|
663
|
+
response = replace_dictionary_entries(dictionary, dictionary_entries, opts)
|
664
|
+
|
665
|
+
response.wait(opts)
|
666
|
+
end
|
667
|
+
|
668
|
+
# Delete entries for a given dictionary
|
669
|
+
#
|
670
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
671
|
+
# @param object_ids [Array<Hash>] array of object ids
|
672
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
673
|
+
#
|
674
|
+
# @return DictionaryResponse
|
675
|
+
#
|
676
|
+
def delete_dictionary_entries(dictionary, object_ids, opts = {})
|
677
|
+
request = object_ids.map do |object_id|
|
678
|
+
{ objectID: object_id }
|
679
|
+
end
|
680
|
+
response = @transporter.write(
|
681
|
+
:POST,
|
682
|
+
path_encode('/1/dictionaries/%s/batch', dictionary),
|
683
|
+
{ clearExistingDictionaryEntries: false, requests: chunk('deleteEntry', request) },
|
684
|
+
opts
|
685
|
+
)
|
686
|
+
|
687
|
+
DictionaryResponse.new(self, response)
|
688
|
+
end
|
689
|
+
|
690
|
+
# Delete entries for a given dictionary and wait for the task to finish
|
691
|
+
#
|
692
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
693
|
+
# @param object_ids [Array<Hash>] array of object ids
|
694
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
602
695
|
#
|
696
|
+
def delete_dictionary_entries!(dictionary, object_ids, opts = {})
|
697
|
+
response = delete_dictionary_entries(dictionary, object_ids, opts)
|
698
|
+
|
699
|
+
response.wait(opts)
|
700
|
+
end
|
701
|
+
|
702
|
+
# Clear all entries for a given dictionary
|
703
|
+
#
|
704
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
705
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
706
|
+
#
|
707
|
+
# @return DictionaryResponse
|
708
|
+
#
|
709
|
+
def clear_dictionary_entries(dictionary, opts = {})
|
710
|
+
replace_dictionary_entries(dictionary, [], opts)
|
711
|
+
end
|
712
|
+
|
713
|
+
# Clear all entries for a given dictionary and wait for the task to finish
|
714
|
+
#
|
715
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
716
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
717
|
+
#
|
718
|
+
def clear_dictionary_entries!(dictionary, opts = {})
|
719
|
+
response = replace_dictionary_entries(dictionary, [], opts)
|
720
|
+
|
721
|
+
response.wait(opts)
|
722
|
+
end
|
723
|
+
|
724
|
+
# Search entries for a given dictionary
|
725
|
+
#
|
726
|
+
# @param dictionary [String] dictionary name. Can be either 'stopwords', 'plurals' or 'compounds'
|
727
|
+
# @param query [String] query to send
|
728
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
729
|
+
#
|
730
|
+
def search_dictionary_entries(dictionary, query, opts = {})
|
731
|
+
@transporter.read(
|
732
|
+
:POST,
|
733
|
+
path_encode('/1/dictionaries/%s/search', dictionary),
|
734
|
+
{ query: query },
|
735
|
+
opts
|
736
|
+
)
|
737
|
+
end
|
738
|
+
|
739
|
+
# Set settings for all the dictionaries
|
740
|
+
#
|
741
|
+
# @param dictionary_settings [Hash]
|
742
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
743
|
+
#
|
744
|
+
# @return DictionaryResponse
|
745
|
+
#
|
746
|
+
def set_dictionary_settings(dictionary_settings, opts = {})
|
747
|
+
response = @transporter.write(:PUT, '/1/dictionaries/*/settings', dictionary_settings, opts)
|
748
|
+
|
749
|
+
DictionaryResponse.new(self, response)
|
750
|
+
end
|
751
|
+
|
752
|
+
# Set settings for all the dictionaries and wait for the task to finish
|
753
|
+
#
|
754
|
+
# @param dictionary_settings [Hash]
|
755
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
756
|
+
#
|
757
|
+
# @return DictionaryResponse
|
758
|
+
#
|
759
|
+
def set_dictionary_settings!(dictionary_settings, opts = {})
|
760
|
+
response = set_dictionary_settings(dictionary_settings, opts)
|
761
|
+
|
762
|
+
response.wait(opts)
|
763
|
+
end
|
764
|
+
|
765
|
+
# Retrieve settings for all the dictionaries
|
766
|
+
#
|
767
|
+
# @param opts [Hash] contains extra parameters to send with your query
|
768
|
+
#
|
769
|
+
def get_dictionary_settings(opts = {})
|
770
|
+
@transporter.read(:GET, '/1/dictionaries/*/settings', {}, opts)
|
771
|
+
end
|
772
|
+
|
773
|
+
# # # # # # # # # # # # # # # # # # # # #
|
774
|
+
# MISC METHODS
|
775
|
+
# # # # # # # # # # # # # # # # # # # # #
|
776
|
+
|
603
777
|
# Method available to make custom requests to the API
|
604
778
|
#
|
605
779
|
def custom_request(data, uri, method, call_type, opts = {})
|
data/lib/algolia/search_index.rb
CHANGED
@@ -987,7 +987,15 @@ module Algolia
|
|
987
987
|
# @return [IndexingResponse]
|
988
988
|
#
|
989
989
|
def set_settings(settings, opts = {})
|
990
|
-
|
990
|
+
request_options = symbolize_hash(opts)
|
991
|
+
forward_to_replicas = request_options.delete(:forwardToReplicas) || false
|
992
|
+
|
993
|
+
response = @transporter.write(
|
994
|
+
:PUT,
|
995
|
+
path_encode('/1/indexes/%s/settings', @name) + handle_params({ forwardToReplicas: forward_to_replicas }),
|
996
|
+
settings,
|
997
|
+
request_options
|
998
|
+
)
|
991
999
|
|
992
1000
|
IndexingResponse.new(self, response)
|
993
1001
|
end
|
@@ -1037,55 +1045,6 @@ module Algolia
|
|
1037
1045
|
|
1038
1046
|
private
|
1039
1047
|
|
1040
|
-
# Check the passed object to determine if it's an array
|
1041
|
-
#
|
1042
|
-
# @param object [Object]
|
1043
|
-
#
|
1044
|
-
def check_array(object)
|
1045
|
-
raise AlgoliaError, 'argument must be an array of objects' unless object.is_a?(Array)
|
1046
|
-
end
|
1047
|
-
|
1048
|
-
# Check the passed object
|
1049
|
-
#
|
1050
|
-
# @param object [Object]
|
1051
|
-
# @param in_array [Boolean] whether the object is an array or not
|
1052
|
-
#
|
1053
|
-
def check_object(object, in_array = false)
|
1054
|
-
case object
|
1055
|
-
when Array
|
1056
|
-
raise AlgoliaError, in_array ? 'argument must be an array of objects' : 'argument must not be an array'
|
1057
|
-
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
1058
|
-
raise AlgoliaError, "argument must be an #{'array of' if in_array} object, got: #{object.inspect}"
|
1059
|
-
end
|
1060
|
-
end
|
1061
|
-
|
1062
|
-
# Check if passed object has a objectID
|
1063
|
-
#
|
1064
|
-
# @param object [Object]
|
1065
|
-
# @param object_id [String]
|
1066
|
-
#
|
1067
|
-
def get_object_id(object, object_id = nil)
|
1068
|
-
check_object(object)
|
1069
|
-
object_id ||= object[:objectID] || object['objectID']
|
1070
|
-
raise AlgoliaError, "Missing 'objectID'" if object_id.nil?
|
1071
|
-
object_id
|
1072
|
-
end
|
1073
|
-
|
1074
|
-
# Build a batch request
|
1075
|
-
#
|
1076
|
-
# @param action [String] action to perform on the engine
|
1077
|
-
# @param objects [Array] objects on which build the action
|
1078
|
-
# @param with_object_id [Boolean] if set to true, check if each object has an objectID set
|
1079
|
-
#
|
1080
|
-
def chunk(action, objects, with_object_id = false)
|
1081
|
-
objects.map do |object|
|
1082
|
-
check_object(object, true)
|
1083
|
-
request = { action: action, body: object }
|
1084
|
-
request[:objectID] = get_object_id(object).to_s if with_object_id
|
1085
|
-
request
|
1086
|
-
end
|
1087
|
-
end
|
1088
|
-
|
1089
1048
|
def raw_batch(requests, opts)
|
1090
1049
|
@transporter.write(:POST, path_encode('/1/indexes/%s/batch', @name), { requests: requests }, opts)
|
1091
1050
|
end
|
data/lib/algolia/version.rb
CHANGED
data/lib/algolia.rb
CHANGED
@@ -7,6 +7,7 @@ require 'algolia/config/base_config'
|
|
7
7
|
require 'algolia/config/search_config'
|
8
8
|
require 'algolia/config/analytics_config'
|
9
9
|
require 'algolia/config/insights_config'
|
10
|
+
require 'algolia/config/recommend_config'
|
10
11
|
require 'algolia/config/recommendation_config'
|
11
12
|
require 'algolia/enums/call_type'
|
12
13
|
require 'algolia/enums/retry_outcome_type'
|
@@ -20,6 +21,7 @@ require 'algolia/responses/indexing_response'
|
|
20
21
|
require 'algolia/responses/add_api_key_response'
|
21
22
|
require 'algolia/responses/update_api_key_response'
|
22
23
|
require 'algolia/responses/delete_api_key_response'
|
24
|
+
require 'algolia/responses/dictionary_response'
|
23
25
|
require 'algolia/responses/restore_api_key_response'
|
24
26
|
require 'algolia/responses/multiple_batch_indexing_response'
|
25
27
|
require 'algolia/responses/multiple_response'
|
@@ -32,6 +34,7 @@ require 'algolia/account_client'
|
|
32
34
|
require 'algolia/search_client'
|
33
35
|
require 'algolia/analytics_client'
|
34
36
|
require 'algolia/insights_client'
|
37
|
+
require 'algolia/recommend_client'
|
35
38
|
require 'algolia/recommendation_client'
|
36
39
|
require 'algolia/error'
|
37
40
|
require 'algolia/search_index'
|
@@ -23,7 +23,9 @@ class AnalyticsClientTest < BaseTest
|
|
23
23
|
endAt: tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ')
|
24
24
|
}
|
25
25
|
|
26
|
-
response =
|
26
|
+
response = retry_test do
|
27
|
+
client.add_ab_test(ab_test)
|
28
|
+
end
|
27
29
|
ab_test_id = response[:abTestID]
|
28
30
|
|
29
31
|
index1.wait_task(response[:taskID])
|
@@ -86,7 +88,9 @@ class AnalyticsClientTest < BaseTest
|
|
86
88
|
endAt: tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ')
|
87
89
|
}
|
88
90
|
|
89
|
-
response =
|
91
|
+
response = retry_test do
|
92
|
+
client.add_ab_test(ab_test)
|
93
|
+
end
|
90
94
|
ab_test_id = response[:abTestID]
|
91
95
|
|
92
96
|
index.wait_task(response[:taskID])
|
@@ -1,25 +1,27 @@
|
|
1
1
|
class MockRequester
|
2
|
+
attr_accessor :requests
|
2
3
|
def initialize
|
3
4
|
@connection = nil
|
5
|
+
@requests = []
|
4
6
|
end
|
5
7
|
|
6
|
-
def send_request(host, method, path,
|
7
|
-
|
8
|
-
response = {
|
9
|
-
connection: connection,
|
8
|
+
def send_request(host, method, path, body, headers, timeout, connect_timeout)
|
9
|
+
request = {
|
10
10
|
host: host,
|
11
|
+
method: method,
|
11
12
|
path: path,
|
13
|
+
body: body,
|
12
14
|
headers: headers,
|
13
|
-
|
14
|
-
|
15
|
-
body: '{"hits":[],"nbHits":0,"page":0,"nbPages":1,"hitsPerPage":20,"exhaustiveNbHits":true,"query":"test","params":"query=test","processingTimeMS":1}',
|
16
|
-
success: true
|
15
|
+
timeout: timeout,
|
16
|
+
connect_timeout: connect_timeout
|
17
17
|
}
|
18
18
|
|
19
|
+
@requests.push(request)
|
20
|
+
|
19
21
|
Algolia::Http::Response.new(
|
20
|
-
status:
|
21
|
-
body:
|
22
|
-
headers:
|
22
|
+
status: 200,
|
23
|
+
body: '{"hits": []}',
|
24
|
+
headers: {}
|
23
25
|
)
|
24
26
|
end
|
25
27
|
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require_relative 'base_test'
|
3
|
+
|
4
|
+
class RecommendClientTest < BaseTest
|
5
|
+
describe 'Recommendations' do
|
6
|
+
def test_get_recommendations
|
7
|
+
requester = MockRequester.new
|
8
|
+
client = Algolia::Recommend::Client.new(@@search_config, http_requester: requester)
|
9
|
+
|
10
|
+
# It correctly formats queries using the 'bought-together' model
|
11
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::BOUGHT_TOGETHER }])
|
12
|
+
|
13
|
+
# It correctly formats queries using the 'related-products' model
|
14
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::RELATED_PRODUCTS }])
|
15
|
+
|
16
|
+
# It correctly formats multiple queries.
|
17
|
+
client.get_recommendations(
|
18
|
+
[
|
19
|
+
{ indexName: 'products', objectID: 'B018APC4LE-1', model: Algolia::Recommend::Model::RELATED_PRODUCTS, threshold: 0 },
|
20
|
+
{ indexName: 'products', objectID: 'B018APC4LE-2', model: Algolia::Recommend::Model::RELATED_PRODUCTS, threshold: 0 }
|
21
|
+
]
|
22
|
+
)
|
23
|
+
|
24
|
+
# It resets the threshold to 0 if it's not numeric.
|
25
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::BOUGHT_TOGETHER, threshold: nil }])
|
26
|
+
|
27
|
+
# It passes the threshold correctly if it's numeric.
|
28
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::BOUGHT_TOGETHER, threshold: 42 }])
|
29
|
+
|
30
|
+
assert_requests(
|
31
|
+
requester,
|
32
|
+
[
|
33
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' },
|
34
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"related-products","threshold":0}]}' },
|
35
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE-1","model":"related-products","threshold":0},{"indexName":"products","objectID":"B018APC4LE-2","model":"related-products","threshold":0}]}' },
|
36
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' },
|
37
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":42}]}' }
|
38
|
+
]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_get_related_products
|
43
|
+
requester = MockRequester.new
|
44
|
+
client = Algolia::Recommend::Client.new(@@search_config, http_requester: requester)
|
45
|
+
|
46
|
+
client.get_related_products([{ indexName: 'products', objectID: 'B018APC4LE' }])
|
47
|
+
|
48
|
+
assert_requests(
|
49
|
+
requester,
|
50
|
+
[{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"related-products","threshold":0}]}' }]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_get_frequently_bought_together
|
55
|
+
requester = MockRequester.new
|
56
|
+
client = Algolia::Recommend::Client.new(@@search_config, http_requester: requester)
|
57
|
+
|
58
|
+
client.get_frequently_bought_together([{ indexName: 'products', objectID: 'B018APC4LE' }])
|
59
|
+
client.get_frequently_bought_together([{ indexName: 'products', objectID: 'B018APC4LE', fallbackParameters: {} }])
|
60
|
+
|
61
|
+
assert_requests(
|
62
|
+
requester,
|
63
|
+
[
|
64
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' },
|
65
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' }
|
66
|
+
]
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'securerandom'
|
1
2
|
require_relative 'base_test'
|
2
3
|
|
3
4
|
class SearchClientTest < BaseTest
|
@@ -215,6 +216,7 @@ class SearchClientTest < BaseTest
|
|
215
216
|
|
216
217
|
def test_api_keys
|
217
218
|
assert_equal ['search'], @api_key[:acl]
|
219
|
+
assert_equal 'A description', @api_key[:description]
|
218
220
|
|
219
221
|
api_keys = @@search_client.list_api_keys[:keys].map do |key|
|
220
222
|
key[:value]
|
@@ -222,7 +224,9 @@ class SearchClientTest < BaseTest
|
|
222
224
|
assert_includes api_keys, @api_key[:value]
|
223
225
|
|
224
226
|
@@search_client.update_api_key!(@api_key[:value], { maxHitsPerQuery: 42 })
|
225
|
-
updated_api_key =
|
227
|
+
updated_api_key = retry_test do
|
228
|
+
@@search_client.get_api_key(@api_key[:value], test: 'test')
|
229
|
+
end
|
226
230
|
assert_equal 42, updated_api_key[:maxHitsPerQuery]
|
227
231
|
|
228
232
|
@@search_client.delete_api_key!(@api_key[:value])
|
@@ -233,18 +237,13 @@ class SearchClientTest < BaseTest
|
|
233
237
|
|
234
238
|
assert_equal 'Key does not exist', exception.message
|
235
239
|
|
236
|
-
|
237
|
-
|
238
|
-
@@search_client.restore_api_key!(@api_key[:value])
|
239
|
-
break
|
240
|
-
rescue Algolia::AlgoliaHttpError => e
|
241
|
-
if e.code != 404
|
242
|
-
raise StandardError
|
243
|
-
end
|
244
|
-
end
|
240
|
+
retry_test do
|
241
|
+
@@search_client.restore_api_key!(@api_key[:value])
|
245
242
|
end
|
246
243
|
|
247
|
-
restored_key =
|
244
|
+
restored_key = retry_test do
|
245
|
+
@@search_client.get_api_key(@api_key[:value])
|
246
|
+
end
|
248
247
|
|
249
248
|
refute_nil restored_key
|
250
249
|
end
|
@@ -333,7 +332,12 @@ class SearchClientTest < BaseTest
|
|
333
332
|
secured_index1 = secured_client.init_index(@index1.name)
|
334
333
|
secured_index2 = secured_client.init_index(@index2.name)
|
335
334
|
|
336
|
-
|
335
|
+
res = retry_test do
|
336
|
+
secured_index1.search('')
|
337
|
+
end
|
338
|
+
|
339
|
+
assert_equal 1, res[:hits].length
|
340
|
+
|
337
341
|
exception = assert_raises Algolia::AlgoliaHttpError do
|
338
342
|
secured_index2.search('')
|
339
343
|
end
|
@@ -366,5 +370,96 @@ class SearchClientTest < BaseTest
|
|
366
370
|
assert_equal 'The SecuredAPIKey doesn\'t have a validUntil parameter.', exception.message
|
367
371
|
end
|
368
372
|
end
|
373
|
+
|
374
|
+
describe 'Custom Dictionaries' do
|
375
|
+
def before_all
|
376
|
+
@client = Algolia::Search::Client.create(APPLICATION_ID_2, ADMIN_KEY_2)
|
377
|
+
end
|
378
|
+
|
379
|
+
def test_stopwords_dictionaries
|
380
|
+
entry_id = SecureRandom.hex
|
381
|
+
assert_equal 0, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
382
|
+
|
383
|
+
entry = {
|
384
|
+
objectID: entry_id,
|
385
|
+
language: 'en',
|
386
|
+
word: 'down'
|
387
|
+
}
|
388
|
+
@client.save_dictionary_entries!('stopwords', [entry])
|
389
|
+
|
390
|
+
stopwords = @client.search_dictionary_entries('stopwords', entry_id)
|
391
|
+
assert_equal 1, stopwords[:nbHits]
|
392
|
+
assert_equal stopwords[:hits][0][:objectID], entry[:objectID]
|
393
|
+
assert_equal stopwords[:hits][0][:word], entry[:word]
|
394
|
+
|
395
|
+
@client.delete_dictionary_entries!('stopwords', [entry_id])
|
396
|
+
assert_equal 0, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
397
|
+
|
398
|
+
old_dictionary_state = @client.search_dictionary_entries('stopwords', '')
|
399
|
+
old_dictionary_entries = old_dictionary_state[:hits].map do |hit|
|
400
|
+
hit.reject { |key| key == :type }
|
401
|
+
end
|
402
|
+
|
403
|
+
@client.save_dictionary_entries!('stopwords', [entry])
|
404
|
+
assert_equal 1, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
405
|
+
|
406
|
+
@client.replace_dictionary_entries!('stopwords', old_dictionary_entries)
|
407
|
+
assert_equal 0, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
408
|
+
|
409
|
+
stopwords_settings = {
|
410
|
+
disableStandardEntries: {
|
411
|
+
stopwords: {
|
412
|
+
en: true
|
413
|
+
}
|
414
|
+
}
|
415
|
+
}
|
416
|
+
|
417
|
+
@client.set_dictionary_settings!(stopwords_settings)
|
418
|
+
|
419
|
+
assert_equal @client.get_dictionary_settings, stopwords_settings
|
420
|
+
end
|
421
|
+
|
422
|
+
def test_plurals_dictionaries
|
423
|
+
entry_id = SecureRandom.hex
|
424
|
+
assert_equal 0, @client.search_dictionary_entries('plurals', entry_id)[:nbHits]
|
425
|
+
|
426
|
+
entry = {
|
427
|
+
objectID: entry_id,
|
428
|
+
language: 'fr',
|
429
|
+
words: %w(cheval chevaux)
|
430
|
+
}
|
431
|
+
@client.save_dictionary_entries!('plurals', [entry])
|
432
|
+
|
433
|
+
plurals = @client.search_dictionary_entries('plurals', entry_id)
|
434
|
+
assert_equal 1, plurals[:nbHits]
|
435
|
+
assert_equal plurals[:hits][0][:objectID], entry[:objectID]
|
436
|
+
assert_equal plurals[:hits][0][:words], entry[:words]
|
437
|
+
|
438
|
+
@client.delete_dictionary_entries!('plurals', [entry_id])
|
439
|
+
assert_equal 0, @client.search_dictionary_entries('plurals', entry_id)[:nbHits]
|
440
|
+
end
|
441
|
+
|
442
|
+
def test_compounds_dictionaries
|
443
|
+
entry_id = SecureRandom.hex
|
444
|
+
assert_equal 0, @client.search_dictionary_entries('compounds', entry_id)[:nbHits]
|
445
|
+
|
446
|
+
entry = {
|
447
|
+
objectID: entry_id,
|
448
|
+
language: 'de',
|
449
|
+
word: 'kopfschmerztablette',
|
450
|
+
decomposition: %w(kopf schmerz tablette)
|
451
|
+
}
|
452
|
+
@client.save_dictionary_entries!('compounds', [entry])
|
453
|
+
|
454
|
+
compounds = @client.search_dictionary_entries('compounds', entry_id)
|
455
|
+
assert_equal 1, compounds[:nbHits]
|
456
|
+
assert_equal compounds[:hits][0][:objectID], entry[:objectID]
|
457
|
+
assert_equal compounds[:hits][0][:word], entry[:word]
|
458
|
+
assert_equal compounds[:hits][0][:decomposition], entry[:decomposition]
|
459
|
+
|
460
|
+
@client.delete_dictionary_entries!('compounds', [entry_id])
|
461
|
+
assert_equal 0, @client.search_dictionary_entries('compounds', entry_id)[:nbHits]
|
462
|
+
end
|
463
|
+
end
|
369
464
|
end
|
370
465
|
end
|
@@ -274,6 +274,9 @@ class SearchIndexTest < BaseTest
|
|
274
274
|
@index.set_settings!(settings)
|
275
275
|
|
276
276
|
assert_equal @index.get_settings, settings
|
277
|
+
|
278
|
+
# check that the forwardToReplicas parameter is passed correctly
|
279
|
+
assert @index.set_settings!(settings, { forwardToReplicas: true })
|
277
280
|
end
|
278
281
|
end
|
279
282
|
|