# Copyright 2023-2026 llmware # Licensed under the Apache License, Version 2.0 (the "License"); you # may not use this file except in compliance with the License. You # may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. See the License for the specific language governing # permissions and limitations under the License. """The retrieval module implements the Query class. The Query class provides a high-level interface for executing a variety of queries on a Library collection, whether instantiated on Mongo, Postgres, or SQLite. The Query class includes both text retrieval strategies, which operate directly as queries on the text collection database, as well as vector embedding semantic retrieval strategies, which require the use of o vector DB and that the embeddings were previously created for the Library. There are also a number of convenience methods that provide 'hybrid' strategies combining elements of semantic and text querying.""" import logging import os from collections import Counter from datetime import datetime try: from bson.objectid import ObjectId except: pass from llmware.configs import LLMWareConfig, LLMWareException, ModelNotFoundException from llmware.embeddings import EmbeddingHandler from llmware.resources import CollectionRetrieval, QueryState from llmware.util import Utilities, CorpTokenizer from llmware.models import ModelCatalog logger = logging.getLogger(__name__) class Query: """Implements the query capabilities against a ``Library` object`. Query is responsible for executing queries against an indexed library. The library can be semantic, text, custom, or hybrid. A query object requires a library object as input, which will be the source of the query. Parameters ---------- library : Library object A ``library`` object. embedding_model : object, default=None An ``embedding_model`` object. tokenizer : object, default=None vector_db_api_key : str, default=None The API key for the vector store. query_id : int, default=None The identifier for a query. This is used when a query state has to be loaded. from_hf : bool, default=False Sets whether the embedding model should be loaded from hugging face. from_sentence_transformer: bool, default=False Sets whether the embedding model should be loaded from ``LLMWareSemanticModel``. embedding_model_name : str, default=None The name of the embedding model. This has to be set if ``from_sentence_transformer=True``. save_history : bool, default=True Sets whether the history of queries should be saved. query_mode : str, default=None Sets the query mode that should be used. It has to be either 'text', 'semantic', or 'hybrid'. vector_db : str, default=None The name of the vector store to be queried against. If it is not set, then this is determined by the given ``embedding_model``. Examples ---------- >>> from llmware.library import Library >>> from llmware.retrieval import Query >>> library = Library().create_new_library('lib_semantic_query') >>> library.add_website(url='https://en.wikipedia.org/wiki/Austria', get_links=False) >>> library.install_new_embedding(embedding_model_name="industry-bert-sec", vector_db="milvus", batch_size=500) >>> query = Query(library=library) >>> results = query.semantic_query(query='the capital of austria is', result_count=3) >>> len(results) 3 >>> results[0].keys() dict_keys(['query', '_id', 'text', 'doc_ID', 'block_ID', 'page_num', 'content_type', 'author_or_speaker', 'special_field1', 'file_source', 'added_to_collection', 'table', 'coords_x', 'coords_y', 'coords_cx', 'coords_cy', 'external_files', 'score', 'similarity', 'distance', 'matches', 'account_name', 'library_name']) >>> results[0]['query'] 'the capital of austria is' >>> results[0]['text'] 'Austria is a parliamentary representative democracy with a popularly elected president as head of ' 'state and a chancellor as head of government and chief executive. Major cities include Vienna , Graz, ' 'Linz , Salzburg , and Innsbruck . Austria has the 17th highest nominal GDP per capita with high ' 'standards of living; it was ranked 25th in the world for its Human Development Index in 2021. ' >>> results[2]['text'] "Austrian Parliament Building Vienna The Parliament of Austria is located in Vienna , the country's capital " "and most populous city. Austria became a federal , representative democratic republic through the " "Federal Constitutional Law of 1920. The political system of the Second Republic with its nine federal " "states is based on the constitution of 1920, amended in 1929, which was re-enacted on 1 May 1945. [108] " """ def __init__(self, library, embedding_model=None, tokenizer=None, vector_db_api_key=None, query_id=None, from_hf=False, from_sentence_transformer=False,embedding_model_name=None, save_history=True, query_mode=None, vector_db=None, model_api_key=None): # load user profile & instantiate core library assets linked to profile self.library = library if library: self.library_name = library.library_name self.account_name = library.account_name else: # throw error if library object does not have library_name and account_name attributes raise LLMWareException(message= f"Query - init - library object not found - {library}") # explicitly pass name of embedding model, if multiple embeddings on library self.embedding_model_name = embedding_model_name # added option to pass embedding_model and tokenizer self.user_passed_model = embedding_model self.user_passed_tokenizer = tokenizer self.from_hf = from_hf self.from_sentence_transformer = from_sentence_transformer # edge case - if a user tries to load a sentence_transformer model but does not pass a model name if from_sentence_transformer and not embedding_model_name: raise LLMWareException(message=f"Query - init - to use sentence_transformers, please " f"provide the model name directly to load") # load default configs # embedding initialization parameters self.query_embedding = None self.embedding_model = None self.embedding_db = None self.embeddings = None self.model_api_key = model_api_key if self.library: self.embeddings = EmbeddingHandler(self.library) self.semantic_distance_threshold = 1000 # basic shut off at such a high level # keys that will be included in query results # full list self.query_result_standard_keys = ["_id", "text", "doc_ID", "block_ID","page_num","content_type", "author_or_speaker", "special_field1", "file_source","added_to_collection", "table", "coords_x", "coords_y", "coords_cx", "coords_cy", "external_files", "score", "similarity", "distance", "matches"] # short_list self.query_result_short_keys = ["text", "file_source", "page_num", "score", "distance","matches"] # minimum_list self.query_result_min_required_keys = ["text", "file_source", "page_num"] # default - set at 'full list' self.query_result_return_keys = self.query_result_standard_keys # default is semantic if embedding in place embedding_record = self.library.get_embedding_status() matched_lib_model = False if embedding_model_name: for emb in embedding_record: logger.info(f"update: Query - embedding record lookup - {embedding_model_name} - {emb}") if emb["embedding_model"] == embedding_model_name: # if no vector_db name passed, then select based only on embedding_model if not vector_db: if emb["embedding_status"] == "yes": self.embedding_db = emb["embedding_db"] self.search_mode = "semantic" matched_lib_model = True break else: # confirm match of pair - embedding_model + vector_db if emb["embedding_db"] == vector_db: if emb["embedding_status"] == "yes": self.embedding_db = emb["embedding_db"] self.search_mode = "semantic" matched_lib_model = True break else: if len(embedding_record) > 0: if not vector_db: last_emb_record = embedding_record[-1] if last_emb_record["embedding_status"] == "yes": self.embedding_db = last_emb_record["embedding_db"] self.search_mode = "semantic" self.embedding_model_name = last_emb_record["embedding_model"] matched_lib_model = True else: # look for match to passed vector_db and take most recent embedding embedding_record.reverse() for embs in embedding_record: if embs["embedding_db"] == vector_db: if embs["embedding_status"] == "yes": self.embedding_db = vector_db self.search_mode = "semantic" self.embedding_model_name = embs["embedding_model"] matched_lib_model = True break if matched_lib_model: logger.info(f"update: Query - found matches in embedding record - " f"{self.embedding_db} - {self.embedding_model_name}") if not self.embedding_model: self.load_embedding_model() else: self.search_mode = "text" # passed for accessing api_based vector db self.vector_db_api_key = vector_db_api_key # if query_id passed, then load that state if query_id: self.query_id = query_id self.load_query_state(query_id) else: self.query_id = QueryState().issue_new_query_id() self.result_text_chunk_size = self.library.block_size_target_characters # state variables self.results = [] self.query_history = [] self.doc_id_list = [] self.doc_fn_list = [] self.save_history = save_history if query_mode: self.search_mode = query_mode # confirm that 'query_history' path exists query_history_path = LLMWareConfig().get_query_path() if not os.path.exists(query_history_path): os.mkdir(query_history_path) os.chmod(query_history_path, 0o777) def load_embedding_model(self): """ Loads the embedding model pulled from the embedding_record of the library. """ # skip if already instantiated self.embedding_model if not self.embedding_model: if self.user_passed_model: if self.from_hf: self.embedding_model = ModelCatalog().load_hf_embedding_model(self.user_passed_model, self.user_passed_tokenizer) if self.from_sentence_transformer: self.embedding_model = ModelCatalog().load_sentence_transformer_model(self.user_passed_model, self.embedding_model_name) else: if ModelCatalog().lookup_model_card(self.embedding_model_name): self.embedding_model = ModelCatalog().load_model(selected_model=self.embedding_model_name, api_key=self.model_api_key) else: logger.info(f"update: Query - selected embedding model could not be found - " f"{self.embedding_model_name}") return self def get_output_keys(self): """ Returns list of keys that will be provided in each query_result. """ return self.query_result_return_keys def set_output_keys(self, result_key_list): """ Sets the list of keys that will be returned in each query_result. """ # set the output keys validated_list = [] for key in result_key_list: if key in self.library.default_keys: validated_list.append(key) # minimum required list for rk in self.query_result_min_required_keys: if rk not in validated_list: validated_list.append(rk) logger.info(f"warning: Query - adding required keys useful in downstream processing - {rk}") # setting updated query_return_keys that is used in packaging query results self.query_result_return_keys = validated_list return validated_list def start_query_session(self, query_id=None): """ Initiates a query session and will capture potentially multiple related queries in single state. """ if query_id: self.query_id = query_id if self.query_id: QueryState(self).load_state(self.query_id) else: query_id = QueryState(self).initiate_new_state_session() return query_id def register_query (self, retrieval_dict): """ Registers a query to the query state. """ # qr_dict = ["query", "results", "doc_ID", "file_source"] # add query results as new "column" in query state self.results += retrieval_dict["results"] if retrieval_dict["query"] not in self.query_history: self.query_history.append(retrieval_dict["query"]) for doc_id in retrieval_dict["doc_ID"]: if doc_id not in self.doc_id_list: self.doc_id_list.append(doc_id) for doc_fn in retrieval_dict["file_source"]: if doc_fn not in self.doc_fn_list: self.doc_fn_list.append(doc_fn) # QueryState(self).save_state(self.query_id) return self def load_query_state(self, query_id): """ Loads a query state of a previous query by query_id """ state = QueryState(self).load_state(query_id) return self def save_query_state(self): """ Saves the current query state. """ QueryState(self).save_state() return self def clear_query_state(self): """ Resets the query state. """ # need to reset state variables QueryState(self).initiate_new_state_session() return self def dump_current_query_state(self): """ Dumps the current query_state to a query_state_dict. """ query_state_dict = {"query_id": self.query_id, "query_history": self.query_history, "results": self.results, "doc_ID": self.doc_id_list, "file_source": self.doc_fn_list } return query_state_dict def query(self, query, query_type="text", result_count=20, results_only=True): """ Main method for executing a basic query - expects query as input, and optional parameters for query_type, result_count and whether results_only. Output is a set of query results, which is a list of dictionaries, with each dictionary representing a single matching retrieval from the collection. """ output_result = {"results": [], "doc_ID": [], "file_source": []} if query_type not in ["text", "semantic"]: logger.error("error: Query().query expects a query type of either 'text' or 'semantic'") return output_result if query_type == "text": output_result = self.text_query(query,result_count=result_count,results_only=results_only) if query_type == "semantic": # check that embedding model is available, and if not, flip to text search if not self.embedding_model: self.load_embedding_model() if self.search_mode == "text" or not self.embedding_model: output_result = self.text_query(query, result_count=result_count,results_only=results_only) else: output_result = self.semantic_query(query, result_count=result_count,results_only=results_only) return output_result def text_query (self, query, exact_mode=False, result_count=20, exhaust_full_cursor=False, results_only=True): """ Execute a basic text query. """ # prepare query if exact match required if exact_mode: query = self.exact_query_prep(query) # query the text collection cursor = CollectionRetrieval(self.library_name,account_name=self.account_name).basic_query(query) # package results, with correct sample counts and output keys requested results_dict = self._cursor_to_qr(query, cursor,result_count=result_count,exhaust_full_cursor= exhaust_full_cursor) if results_only: return results_dict["results"] return results_dict def text_query_with_document_filter(self, query, doc_filter, result_count=20, exhaust_full_cursor=False, results_only=True, exact_mode=False): """ Execute a text query with a document filter applied. """ # prepare query if exact match required if exact_mode: query = self.exact_query_prep(query) key = None value_range = [] if "doc_ID" in doc_filter: key = "doc_ID" value_range = doc_filter["doc_ID"] elif "file_source" in doc_filter: key = "file_source" value_range = doc_filter["file_source"] else: logger.warning("warning: Query - expected to receive document filter with keys of 'doc_ID' or " "'file_source' - as a safe fall-back - will run the requested query without a filter.") if key: cursor = CollectionRetrieval(self.library_name, account_name=self.account_name). \ text_search_with_key_value_range(query, key, value_range) else: # as fallback, if no key found, then run query without filter cursor = CollectionRetrieval(self.library_name, account_name=self.account_name).basic_query(query) result_dict = self._cursor_to_qr(query, cursor, result_count=result_count, exhaust_full_cursor=exhaust_full_cursor) if results_only: return result_dict["results"] return result_dict def text_query_by_content_type (self, query, content_type,results_only=True): """ Execute a text query with additional constraint of content type, e.g., 'image', 'table', or 'text' """ filter_dict = {"content_type": content_type} retrieval_dict = self.text_query_with_custom_filter(query,filter_dict,results_only=True) return retrieval_dict def image_query(self, query, results_only=True): """ Execute a query with content_type == 'image'. """ filter_dict = {"content_type": "image"} retrieval_dict = self.text_query_with_custom_filter(query, filter_dict,results_only=True) return retrieval_dict def table_query(self, query, export_tables_to_csv=False, results_only=True): """ Execute a query with content_type == 'table'. """ filter_dict = {"content_type": "table"} retrieval_dict = self.text_query_with_custom_filter(query, filter_dict,results_only=True) # output and write tables to csv files if export_tables_to_csv: for i, entry in enumerate(retrieval_dict["results"]): f = self.export_one_table_to_csv(entry,output_fp=LLMWareConfig.get_query_path(), output_fn="table_{}.csv".format(i)) logger.warning(f"update: csv created - {LLMWareConfig.get_query_path()}- {f}") return retrieval_dict def text_search_by_page (self, query, page_num=1, results_only=True): """ Execute a text search with page number constraint provided as page_num parameter. """ key = "master_index" # parsing uses "master_index" across multiple input sources, interpret as "page_num" if not isinstance(page_num, list): page_num = [page_num] cursor_results = CollectionRetrieval(self.library_name, account_name=self.account_name).\ text_search_with_key_value_range(query, key, page_num) retrieval_dict = self._cursor_to_qr(query, cursor_results) if results_only: return retrieval_dict["results"] return retrieval_dict def text_query_by_author_or_speaker(self, query, author_or_speaker, results_only=True): """ Execute a text query with specific author_or_speaker constraint. """ filter_dict = {"author_or_speaker": author_or_speaker} retrieval_dict = self.text_query_with_custom_filter(query,filter_dict,results_only=results_only) return retrieval_dict def text_query_with_custom_filter (self, query, filter_dict, result_count=20, exhaust_full_cursor=False, results_only=True): """ Execute a text query with additional custom filter dictionary. """ # filter_dict is a dict with indefinite number of key:value pairs - each key will be interpreted # as "$and" in the query, requiring a match against all of the key:values in the filter_dict # validate filter dict validated_filter_dict = {} for key, values in filter_dict.items(): for valid_keys in self.library.default_keys: if key in valid_keys: validated_filter_dict.update({key:values}) if validated_filter_dict: cursor = CollectionRetrieval(self.library_name, account_name=self.account_name).\ text_search_with_key_value_dict_filter(query,validated_filter_dict) else: logger.error("error: Query text_query_with_custom_filter - keys in filter_dict are not" "recognized as part of the library.collection default_keys list.") return -1 result_dict = self._cursor_to_qr_with_secondary_filter(query, cursor,filter_dict, result_count=result_count, exhaust_full_cursor=exhaust_full_cursor) if results_only: return result_dict["results"] return result_dict def _cursor_to_qr_with_secondary_filter(self, query, cursor_results, filter_dict, result_count=20, exhaust_full_cursor=False): """ Internal helper method to package query results from cursor. """ qr = [] counter = 0 doc_id_list = [] doc_fn_list = [] for raw_qr in cursor_results: # update to locate match and add to result matches_found = self.locate_query_match(query, raw_qr["text"]) raw_qr.update({"matches": matches_found}) raw_qr.update({"page_num": raw_qr["master_index"]}) raw_qr.update({"_id": str(raw_qr["_id"])}) if "score" not in raw_qr: raw_qr.update({"score": 0.0}) if "similarity" not in raw_qr: raw_qr.update({"similarity": 0.0}) if "distance" not in raw_qr: raw_qr.update({"distance": 0.0}) # apply secondary filter dict match = -1 for key, value in filter_dict.items(): if key in raw_qr: # support case in which filter_dict is a list, e.g., doc_id = {5,9,13} if raw_qr[key] == value or (isinstance(value,list) and raw_qr[key] in value): match = 1 else: match = -1 break if match == 1: # output target keys output_dict = {} output_dict.update({"query": query}) for key in self.query_result_return_keys: if key not in raw_qr: logger.warning(f"warning: Query() - selected output key not found in result - {key}") else: output_dict.update({key: raw_qr[key]}) output_dict.update({"account_name": self.account_name}) output_dict.update({"library_name": self.library_name}) qr.append(output_dict) if raw_qr["doc_ID"] not in doc_id_list: doc_id_list.append(raw_qr["doc_ID"]) doc_fn_list.append(raw_qr["file_source"]) counter += 1 # will exhaust full cursor if .exhaust_full_cursor = True if counter >= result_count and not exhaust_full_cursor: break qr_dict = {"query": query, "results": qr, "doc_ID": doc_id_list, "file_source": doc_fn_list} if self.save_history: self.register_query(qr_dict) return qr_dict def _cursor_to_qr (self, query, cursor_results, result_count=20, exhaust_full_cursor=False): """ Internal helper method to package query results from cursor. """ qr = [] counter = 0 doc_id_list = [] doc_fn_list = [] for raw_qr in cursor_results: # update to locate match and add to result matches_found = self.locate_query_match(query, raw_qr["text"]) raw_qr.update({"matches": matches_found}) raw_qr.update({"page_num": raw_qr["master_index"]}) raw_qr.update({"_id": str(raw_qr["_id"])}) if "score" not in raw_qr: raw_qr.update({"score": 0.0}) if "similarity" not in raw_qr: raw_qr.update({"similarity": 0.0}) if "distance" not in raw_qr: raw_qr.update({"distance": 0.0}) # output target keys output_dict = {} output_dict.update({"query": query}) for key in self.query_result_return_keys: if key not in raw_qr: logger.warning(f"warning: Query() - selected output key not found in result - {key}") else: output_dict.update({key: raw_qr[key]}) output_dict.update({"account_name": self.account_name}) output_dict.update({"library_name": self.library_name}) qr.append(output_dict) if raw_qr["doc_ID"] not in doc_id_list: doc_id_list.append(raw_qr["doc_ID"]) doc_fn_list.append(raw_qr["file_source"]) counter += 1 # will exhaust full cursor if .exhaust_full_cursor = True if counter >= result_count and not exhaust_full_cursor: break qr_dict = {"query": query,"results": qr, "doc_ID": doc_id_list, "file_source": doc_fn_list} if self.save_history: self.register_query(qr_dict) return qr_dict def semantic_query(self, query, result_count=20, embedding_distance_threshold=None, custom_filter=None, results_only=True): """ Main method to execute a semantic query - only required parameter is the query. """ if not embedding_distance_threshold: embedding_distance_threshold = self.semantic_distance_threshold self.load_embedding_model() # confirm that embedding model exists, or catch and raise error if self.embedding_model: self.query_embedding = self.embedding_model.embedding(query) else: raise ModelNotFoundException(self.library_name) if self.embedding_db and self.embedding_model: semantic_block_results = self.embeddings.search_index(self.query_embedding, embedding_db=self.embedding_db, model=self.embedding_model, sample_count=result_count) else: logger.error(f"error: Query - embedding record does not indicate embedding db - " f"{self.embedding_db} and/or embedding model - {self.embedding_model}") raise LLMWareException(message=f"Query - semantic query - selected " f"embedding database is not supported - {self.embedding_db}") qr_raw = [] # Collecting semantic results for i, blocks in enumerate(semantic_block_results): if blocks[1] < embedding_distance_threshold: block_data = blocks[0] block_data["distance"] = blocks[1] block_data["semantic"] = "semantic" block_data["score"] = 0.0 qr_raw.append(block_data) # Applying custom filter if provided if custom_filter: qr_raw = self.apply_custom_filter(qr_raw, custom_filter) # Processing results results_dict = self._cursor_to_qr(query, qr_raw, result_count=result_count) return results_dict["results"] if results_only else results_dict def apply_custom_filter(self, results, custom_filter): """ Apply custom filter to a set of results. """ filtered_results = [] for result in results: if all(result.get(key) == value for key, value in custom_filter.items()): filtered_results.append(result) return filtered_results def semantic_query_with_document_filter(self, query, filter_dict, embedding_distance_threshold=None, result_count=100, results_only=True): """ Execute semantic query with secondary constraint of document filter. """ # checks for filter to offer option to do semantic query in specific doc, page or content range if not embedding_distance_threshold: embedding_distance_threshold = self.semantic_distance_threshold # note: by default, retrieves a much larger set of results to try to account for filter th = self.semantic_distance_threshold # confirm that embedding model exists, or catch and raise error if self.embedding_model: self.query_embedding = self.embedding_model.embedding(query) else: raise ModelNotFoundException(self.library_name) if self.embedding_db and self.embedding_model: semantic_block_results = self.embeddings.search_index(self.query_embedding, embedding_db=self.embedding_db, model=self.embedding_model, sample_count=result_count) else: logger.error(f"error: Query - embedding record does not indicate embedding db- {self.embedding_db} " f"and/or an embedding_model - {self.embedding_model}") raise LLMWareException(message=f"Query - semantic query with document filter - selected " f"embedding database is not supported - {self.embedding_db}") qr_raw = [] # may need to conform the output structure of semantic_block_results for i, blocks in enumerate(semantic_block_results): # assume that each block has at least two components: [0] core mongo block, and [1] distance metric if blocks[1] < embedding_distance_threshold: blocks[0].update({"distance": blocks[1]}) blocks[0].update({"semantic": "semantic"}) blocks[0].update({"score": 0.0}) qr_raw.append(blocks[0]) result_output = self._cursor_to_qr_with_secondary_filter(query,qr_raw,filter_dict,result_count=result_count) if results_only: return result_output["results"] return result_output def similar_blocks_embedding(self, block, result_count=20, embedding_distance_threshold=10, results_only=True): """ Application of semantic embedding space - takes a block of text as input and finds most similar comparable text chunks. If you are comfortable with the normalization of the embedding vector space, then set a specific embedding_distance_threshold, e.g., embedding_distance_threshold=1.1 to limit the results to text blocks within a distance of 1.1 in the embedding space.""" # will use embedding to find similar blocks from a given block # confirm that embedding model exists, or catch and raise error if self.embedding_model: self.query_embedding = self.embedding_model.embedding(block["text"]) else: raise ModelNotFoundException(self.library_name) if self.embedding_model and self.embedding_db: semantic_block_results = self.embeddings.search_index(self.query_embedding, embedding_db=self.embedding_db, model=self.embedding_model, sample_count=result_count) else: logger.error(f"error: Query - embedding record does not indicate embedding db- " f"{self.embedding_db} and/or embedding model - {self.embedding_model}") raise LLMWareException(message=f"Query - similar blocks embedding - selected " f"embedding database is not supported - {self.embedding_db}") qr_raw = [] # may need to conform the output structure of semantic_block_results for i, blocks in enumerate(semantic_block_results): # assume that each block has at least two components: [0] core mongo block, and [1] distance metric if blocks[1] < embedding_distance_threshold: blocks[0].update({"distance": blocks[1]}) blocks[0].update({"semantic": "semantic"}) blocks[0].update({"score": 0.0}) qr_raw.append(blocks[0]) # pick up with boilerplate results_dict = self._cursor_to_qr("", qr_raw, result_count=result_count) if results_only: return results_dict["results"] return results_dict def dual_pass_query(self, query, result_count=20, primary="text", safety_check=True, custom_filter=None, results_only=True): """ Executes a combination of text and semantic queries and attempts to interweave and re-rank based on correspondence between the two query attempts. """ # safety check if safety_check and result_count > 100: logger.info("warning: Query().dual_pass_query runs a comparison of output rankings using semantic " "and text. This particular implementation is not optimized for sample lists longer " "than ~100 X 100. To remove this warning, there are two options - (1) set the " "safety_check to False in the method declaration, or (2) keep sample count below 100.") result_count = 100 # following keys are required for dual pass query to work, add them if user has omitted them keys_to_check = ['_id', 'doc_ID'] for key in keys_to_check: if key not in self.query_result_return_keys: self.query_result_return_keys.append(key) # run dual pass - text + semantic # Choose appropriate text query method based on custom_filter if custom_filter: retrieval_dict_text = self.text_query_with_custom_filter(query, custom_filter, result_count=result_count, results_only=True) else: retrieval_dict_text = self.text_query(query, result_count=result_count, results_only=True) # Semantic query with custom filter retrieval_dict_semantic = self.semantic_query(query, result_count=result_count, custom_filter=custom_filter, results_only=True) if primary == "text": first_list = retrieval_dict_text second_list = retrieval_dict_semantic else: first_list = retrieval_dict_semantic second_list = retrieval_dict_text confirming_list = [] primary_only = [] secondary_only = [] matched_second_list = [] # this is the time intensive "n-squared" loop - probably OK up to 100+ for i, entry in enumerate(first_list): match = -1 for j, entry2 in enumerate(second_list): if entry["_id"] == entry2["_id"]: entry["match_status"] = "matched" # Tagging as matched confirming_list.append(entry) match = 1 matched_second_list.append(entry2["_id"]) break if match == -1: entry["match_status"] = "primary_only" # Tagging as primary only primary_only.append(entry) for k, entry2 in enumerate(second_list): if entry2["_id"] not in matched_second_list: entry2["match_status"] = "secondary_only" # Tagging as secondary only secondary_only.append(entry2) # assemble merged top results merged_results = [] merged_results += confirming_list select_primary = min(len(primary_only), 5) select_secondary = min(len(secondary_only), 5) merged_results += primary_only[0:select_primary] merged_results += secondary_only[0:select_secondary] doc_id_list = [] doc_fn_list = [] for qr in merged_results: if qr["doc_ID"] not in doc_id_list: doc_id_list.append(qr["doc_ID"]) if qr["file_source"] not in doc_fn_list: doc_fn_list.append(qr["file_source"]) retrieval_dict = {"results": merged_results, "text_results": retrieval_dict_text, "semantic_results": retrieval_dict_semantic, "doc_ID": doc_id_list, "file_source": doc_fn_list} if results_only: return merged_results return retrieval_dict def augment_qr (self, query_result, query_topic, augment_query="semantic"): """ Augments the set of query results using alternative retrieval strategy. """ if augment_query == "semantic": qr_aug = self.semantic_query(query_topic,result_count=20, results_only=True) else: qr_aug = self.text_query(query_topic,result_count=20, results_only=True) # consolidate the qr lists updated_qr = [] for qr in query_result: updated_qr.append(qr) # start with original qr list # add up to 10 entries from semantic list semantic_return_max = 10 for j, sem_entries in enumerate(qr_aug): if sem_entries not in updated_qr: updated_qr.append(sem_entries) if j > semantic_return_max: break return updated_qr def apply_semantic_ranking(self, qr, issue_semantic): """ Apply ranking of query results by semantic query ranking. """ # designed to take a set of query results, and re-rank the order of results by their semantic distance # --note: possible to use a different query term for issue_semantic than the original query result # heuristic - look for result targets of at least 20, but up to the exact len of the qr result_target = max(len(qr),20) semantic_qr = self.semantic_query(issue_semantic,result_count=result_target) reranked_qr = [] for i, s in enumerate(semantic_qr): for q in qr: if s["_id"] == q["_id"]: reranked_qr.append(q) break for q in qr: if q not in reranked_qr: reranked_qr.append(q) return reranked_qr def document_filter (self, filter_topic, query_mode="text", result_count=30, exact_mode = False, exhaust_full_cursor=True): """ Takes a filter_topic as input, along with query_mode, and generates a document filter as output. """ result_dict = None if query_mode not in ["text", "semantic", "hybrid"]: logger.error(f"error: Query document_filter supports query types - 'text', " f"'semantic', and 'hybrid' - type selected not recognized - {query_mode}") return result_dict if query_mode == "text": result_dict = self.text_query(filter_topic,exact_mode=exact_mode,result_count=result_count, exhaust_full_cursor=exhaust_full_cursor,results_only=False) if query_mode == "semantic": result_dict = self.semantic_query(filter_topic,result_count=result_count, results_only=False) if query_mode == "hybrid": result_dict = self.dual_pass_query(filter_topic) if not result_dict: logger.error(f"error: Query file_selector_only could not find a result - unexpected error - " f"{filter_topic}") return result_dict doc_filter_output = {"doc_ID": result_dict["doc_ID"], "file_source": result_dict["file_source"]} return doc_filter_output def page_lookup(self, page_list=None, doc_id_list=None, text_only=False): """ Look up by specific pages, e.g, useful to retrieve the entire first page of a template document. """ if not doc_id_list: doc_id_list = self.list_doc_id() else: if isinstance(doc_id_list,dict): if "doc_ID" in doc_id_list: doc_id_list = doc_id_list["doc_ID"] else: logger.warning("warning: could not recognize doc id list requested. by default, " "will set to all documents in the library collection.") doc_id_list = self.list_doc_id() if not page_list: logger.warning("warning: page lookup requested, but no value range identified. by default, will set " "to retrieve the first page only.") page_list = [1] if text_only: page_dict = {"doc_ID": {"$in":doc_id_list}, "master_index": {"$in": page_list}, "content_type":"text"} else: page_dict = {"doc_ID": {"$in":doc_id_list}, "master_index": {"$in": page_list}} cursor_results = CollectionRetrieval(self.library_name, account_name=self.account_name).filter_by_key_dict(page_dict) output = [] for x in cursor_results: x.update({"matches": []}) x.update({"page_num": x["master_index"]}) output.append(x) return output def get_whole_library(self, selected_keys=None): """ Gets the whole library - and will return as a list in-memory. """ match_results_cursor = CollectionRetrieval(self.library_name, account_name=self.account_name).get_whole_collection() match_results = match_results_cursor.pull_all() qr = [] # option to retrieve only user selected keys if not selected_keys: selected_keys = self.library.default_keys for i, block in enumerate(match_results): new_row = {} new_row.update({"_id": str(block["_id"])}) new_row.update({"matches": []}) new_row.update({"page_num": block["master_index"]}) new_row.update({"score": 0.0}) new_row.update({"similarity": 0.0}) new_row.update({"distance": 0.0}) for keys in selected_keys: if keys in block: if keys not in new_row: new_row.update({keys:block[keys]}) qr.append(new_row) return qr def export_all_tables(self, query="", output_fp=None): """ Exports all tables, with query option to limit the list from a library. """ table_csv_files_created = [] if not output_fp: output_fp = self.library.misc_path if not query: match_results = CollectionRetrieval(self.library_name, account_name=self.account_name).filter_by_key("content_type","table") else: kv_dict = {"content_type": "table"} match_results = CollectionRetrieval(self.library_name, account_name=self.account_name).\ text_search_with_key_value_dict_filter(query,kv_dict) counter = 0 for i, entries in enumerate(match_results): table = entries["table"] output = [] table_raw = table rows = table_raw.split("