Skip to content

Commit f6d2510

Browse files
authored
Redis vector-store improvements (#2434)
* Use vector similarity in results * Note difference in functionality * Clarify future change * Clean up TODOs * Allow vector store to be configured * Add docs link * Clean up duplicate code
1 parent c5c09b3 commit f6d2510

7 files changed

Lines changed: 457 additions & 115 deletions

File tree

chatterbot/conversation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def __init__(self, text: str, in_response_to=None, **kwargs):
101101
# This is the confidence with which the chat bot believes
102102
# this is an accurate response. This value is set when the
103103
# statement is returned by the chat bot.
104-
self.confidence = 0
104+
self.confidence = kwargs.get('confidence', 0)
105105

106106
self.storage = None
107107

chatterbot/logic/best_match.py

Lines changed: 89 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -41,83 +41,110 @@ def process(self, input_statement: Statement, additional_response_selection_para
4141
break
4242

4343
self.chatbot.logger.info('Selecting "{}" as a response to "{}" with a confidence of {}'.format(
44-
closest_match.in_response_to, input_statement.text, closest_match.confidence
44+
closest_match.text, input_statement.text, closest_match.confidence
4545
))
4646

47-
recent_repeated_responses = filters.get_recent_repeated_responses(
48-
self.chatbot,
49-
input_statement.conversation
50-
)
51-
52-
for index, recent_repeated_response in enumerate(recent_repeated_responses):
53-
self.chatbot.logger.info('{}. Excluding recent repeated response of "{}"'.format(
54-
index, recent_repeated_response
55-
))
56-
57-
response_selection_parameters = {
58-
'search_text': closest_match.search_text,
59-
'persona_not_startswith': 'bot:',
60-
'exclude_text': recent_repeated_responses,
61-
'exclude_text_words': self.excluded_words
62-
}
63-
64-
alternate_response_selection_parameters = {
65-
'search_in_response_to': self.chatbot.tagger.get_text_index_string(
66-
input_statement.text
67-
),
68-
'persona_not_startswith': 'bot:',
69-
'exclude_text': recent_repeated_responses,
70-
'exclude_text_words': self.excluded_words
71-
}
72-
73-
if additional_response_selection_parameters:
74-
response_selection_parameters.update(
75-
additional_response_selection_parameters
76-
)
77-
alternate_response_selection_parameters.update(
78-
additional_response_selection_parameters
47+
# Semantic vector search vs indexed text search have different architectures:
48+
#
49+
# For SQL with indexed text search:
50+
# - Phase 1 finds a match based on string similarity (Levenshtein distance)
51+
# - Phase 2 finds variations of that match to get diverse responses
52+
# - This makes sense because you might have multiple instances of similar statements
53+
# learned from different conversations that provide different response options
54+
#
55+
# For Redis with semantic vectors:
56+
# - Phase 1 finds semantically similar responses using vector embeddings
57+
# - The semantic similarity already captures the "closeness" we want
58+
# - Phase 2 would be redundant - we already have the best semantic match
59+
# - The vector search inherently considers the entire semantic space, not just
60+
# exact string matches, so additional variation searching is unnecessary
61+
#
62+
# NOTE: This difference of functionality may need to be modified in the future
63+
# if the redis adapter is determined to benefit from a Phase 2 style response
64+
# selection. The main symptom that would drive such a change would be low
65+
# quality or repetitive responses when using semantic vector search.
66+
#
67+
# Therefore, semantic vector search returns the Phase 1 result directly.
68+
if self.search_algorithm.name == 'semantic_vector_search' and closest_match.confidence > 0:
69+
response = closest_match
70+
self.chatbot.logger.info('Using semantic search result directly: "{}"'.format(response.text))
71+
else:
72+
# For other search algorithms (indexed_text_search, text_search),
73+
# we need to find responses to the closest match
74+
recent_repeated_responses = filters.get_recent_repeated_responses(
75+
self.chatbot,
76+
input_statement.conversation
7977
)
8078

81-
# Get all statements with text similar to the closest match
82-
response_list = list(self.chatbot.storage.filter(**response_selection_parameters))
83-
84-
if response_list:
85-
response = self.select_response(
86-
input_statement,
87-
response_list,
88-
self.chatbot.storage
89-
)
79+
for index, recent_repeated_response in enumerate(recent_repeated_responses):
80+
self.chatbot.logger.info('{}. Excluding recent repeated response of "{}"'.format(
81+
index, recent_repeated_response
82+
))
9083

91-
response.confidence = closest_match.confidence
92-
self.chatbot.logger.info('Selecting "{}" from {} optimal responses.'.format(
93-
response.text,
94-
len(response_list)
95-
))
96-
else:
97-
'''
98-
The case where there was no responses returned for the selected match
99-
but a value exists for the statement the match is in response to.
100-
'''
101-
self.chatbot.logger.info('No responses found. Generating alternate response list.')
84+
response_selection_parameters = {
85+
'search_text': closest_match.search_text,
86+
'persona_not_startswith': 'bot:',
87+
'exclude_text': recent_repeated_responses,
88+
'exclude_text_words': self.excluded_words
89+
}
90+
91+
alternate_response_selection_parameters = {
92+
'search_in_response_to': self.chatbot.tagger.get_text_index_string(
93+
input_statement.text
94+
),
95+
'persona_not_startswith': 'bot:',
96+
'exclude_text': recent_repeated_responses,
97+
'exclude_text_words': self.excluded_words
98+
}
99+
100+
if additional_response_selection_parameters:
101+
response_selection_parameters.update(
102+
additional_response_selection_parameters
103+
)
104+
alternate_response_selection_parameters.update(
105+
additional_response_selection_parameters
106+
)
102107

103-
alternate_response_list = list(self.chatbot.storage.filter(
104-
**alternate_response_selection_parameters
105-
))
108+
# Get all statements with text similar to the closest match
109+
response_list = list(self.chatbot.storage.filter(**response_selection_parameters))
106110

107-
if alternate_response_list:
111+
if response_list:
108112
response = self.select_response(
109113
input_statement,
110-
alternate_response_list,
114+
response_list,
111115
self.chatbot.storage
112116
)
113117

114118
response.confidence = closest_match.confidence
115-
self.chatbot.logger.info('Selected alternative response "{}" from {} options'.format(
119+
self.chatbot.logger.info('Selecting "{}" from {} optimal responses.'.format(
116120
response.text,
117-
len(alternate_response_list)
121+
len(response_list)
118122
))
119123
else:
120-
response = self.get_default_response(input_statement)
121-
self.chatbot.logger.info('Using "%s" as a default response.', response.text)
124+
'''
125+
The case where there was no responses returned for the selected match
126+
but a value exists for the statement the match is in response to.
127+
'''
128+
self.chatbot.logger.info('No responses found. Generating alternate response list.')
129+
130+
alternate_response_list = list(self.chatbot.storage.filter(
131+
**alternate_response_selection_parameters
132+
))
133+
134+
if alternate_response_list:
135+
response = self.select_response(
136+
input_statement,
137+
alternate_response_list,
138+
self.chatbot.storage
139+
)
140+
141+
response.confidence = closest_match.confidence
142+
self.chatbot.logger.info('Selected alternative response "{}" from {} options'.format(
143+
response.text,
144+
len(alternate_response_list)
145+
))
146+
else:
147+
response = self.get_default_response(input_statement)
148+
self.chatbot.logger.info('Using "%s" as a default response.', response.text)
122149

123150
return response

chatterbot/search.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,27 @@ class SemanticVectorSearch:
165165
Does not require a tagger or comparison function - relies on the storage
166166
adapter's native vector similarity search capabilities.
167167
168+
This search algorithm is designed for vector-based storage adapters like
169+
RedisVectorStorageAdapter. Unlike indexed text search (IndexedTextSearch)
170+
which uses string matching and requires a two-phase search, semantic vector
171+
search finds the best matching response in a single phase using vector
172+
similarity.
173+
174+
Architecture differences:
175+
-------------------------
176+
Indexed Text Search (SQL adapters):
177+
- Phase 1: Find statements with string similarity to input
178+
- Phase 2: Find variations of the match to get diverse responses
179+
- Requires search_text and search_in_response_to indexed fields
180+
- Uses comparison functions (Levenshtein, Jaccard, etc.)
181+
182+
Semantic Vector Search (Redis adapter):
183+
- Single phase: Find statements with vector similarity to input
184+
- Semantic embeddings capture contextual meaning
185+
- No need for Phase 2 - vector similarity already provides the best match
186+
- Does not use search_text/search_in_response_to fields
187+
- Confidence scores based on cosine distance in vector space
188+
168189
:param search_page_size:
169190
The maximum number of records to load into memory at a time when searching.
170191
Defaults to 1000
@@ -212,8 +233,8 @@ def search(self, input_statement, **additional_parameters):
212233

213234
# Yield statements with confidence scores from vector similarity
214235
for statement in statement_list:
215-
# Confidence should already be set by the storage adapter
216-
confidence = getattr(statement, 'confidence', 0.0)
236+
# Confidence is set by the storage adapter during filter()
237+
confidence = statement.confidence
217238

218239
if confidence > best_confidence_so_far:
219240
best_confidence_so_far = confidence

0 commit comments

Comments
 (0)