diff --git a/docker-compose.yaml b/docker-compose.yaml index 7556703b..17ca452c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,7 +44,8 @@ services: entrypoint: [ "gunicorn", "--workers=$API_WORKERS", "--name=dug", "--bind=0.0.0.0:$API_PORT", "--timeout=$API_TIMEOUT", - "--log-level=DEBUG", "--enable-stdio-inheritance", "--reload", "dug.api:app" ] + "--log-level=DEBUG", "--enable-stdio-inheritance", + "-k", "uvicorn.workers.UvicornWorker", "--reload", "dug.server:APP" ] volumes: - ./src:/home/dug/dug/ ports: diff --git a/requirements.txt b/requirements.txt index 8c99c919..eae6fcf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,39 +1,18 @@ -aniso8601==9.0.1 -attrs==20.3.0 -biolinkml==1.4.9 -certifi==2020.12.5 -chardet==3.0.4 -click==7.1.2 -elasticsearch==7.12.0 -flake8==3.9.0 -flasgger==0.9.5 -Flask==1.1.1 -Flask-Cors==3.0.9 -Flask-RESTful==0.3.8 -gunicorn==20.0.4 -idna==2.8 -itsdangerous==1.1.0 -Jinja2==2.11.3 -jsonschema==3.2.0 -MarkupSafe==1.1.1 -mistune==0.8.4 -pluggy==0.13.1 +elasticsearch[async]==7.16.3 +fastapi==0.95.0 +uvicorn +gunicorn +itsdangerous +Jinja2 +jsonschema +MarkupSafe +mistune==2.0.3 +pluggy==1.0.0 pyrsistent==0.17.3 -pytest==6.2.2 +pytest pytz==2021.1 -PyYAML==5.4.1 -redis==3.4.1 -requests==2.22.0 -requests-cache==0.5.2 -six==1.15.0 -Sphinx==2.4.4 -sphinx-click==2.3.1 -sphinx-rtd-theme==0.4.3 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==1.0.3 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.4 -urllib3==1.25.11 -Werkzeug==0.16.1 +PyYAML==6.0 +redis==4.4.2 +requests==2.28.2 +requests-cache==0.9.8 +six==1.16.0 diff --git a/src/dug/_version.py b/src/dug/_version.py index c1467c65..630adaeb 100644 --- a/src/dug/_version.py +++ b/src/dug/_version.py @@ -1 +1 @@ -__version__ = "2.9.6" +__version__ = "2.9.8" diff --git a/src/dug/api.py b/src/dug/api.py deleted file mode 100644 index 12ef03f8..00000000 --- a/src/dug/api.py +++ /dev/null @@ -1,342 +0,0 @@ -import argparse -import json -import logging -import os -import sys -import traceback - -import jsonschema -import yaml -from flasgger import Swagger -from flask import Flask, g, Response, request -from flask_cors import CORS -from flask_restful import Api, Resource - -from dug.config import Config -from dug.core.search import Search - -""" -Defines the semantic search API - -This exists in large part because it's not safe to expose the Elasticsearch interface to the internet. -So we'll need to add validation to ensure reasonable reasonable, well formed, valid requests inbound. -""" -logger = logging.getLogger (__name__) - -app = Flask(__name__) - -""" Enable CORS. """ -api = Api(app) -CORS(app) -debug=False - -""" Load the schema. """ -schema_file_path = os.path.join (os.path.dirname (__file__), 'conf', 'api-schema.yaml') -template = None -with open(schema_file_path, 'r') as file_obj: - template = yaml.load(file_obj, Loader=yaml.FullLoader) - -""" Describe the API. """ -app.config['SWAGGER'] = { - 'title': 'Dug Search API', - 'description': 'An API, compiler, and executor for cloud native distributed systems.', - 'uiversion': 3 -} - -swagger = Swagger(app, template=template) - -def dug (): - if not hasattr(g, 'dug'): - g.search = Search(Config.from_env()) - return g.search - -class DugResource(Resource): - """ Base class handler for Dug API requests. """ - def __init__(self): - self.specs = {} - - """ Functionality common to Dug services. """ - def validate (self, request, component): - return - """ Validate a request against the schema. """ - if not self.specs: - with open(schema_file_path, 'r') as file_obj: - self.specs = yaml.load(file_obj) - to_validate = self.specs["components"]["schemas"][component] - try: - app.logger.debug (f"--:Validating obj {json.dumps(request.json, indent=2)}") - app.logger.debug (f" schema: {json.dumps(to_validate, indent=2)}") - jsonschema.validate(request.json, to_validate) - except jsonschema.exceptions.ValidationError as error: - app.logger.error (f"ERROR: {str(error)}") - traceback.print_exc (error) - abort(Response(str(error), 400)) - - def create_response (self, result=None, status='success', message='', exception=None): - """ Create a response. Handle formatting and modifiation of status for exceptions. """ - if exception: - traceback.print_exc () - status='error' - exc_type, exc_value, exc_traceback = sys.exc_info() - result = { - 'error' : repr(traceback.format_exception(exc_type, exc_value, exc_traceback)) - } - return { - 'status' : status, - 'result' : result, - 'message' : message - } - -class DugSearchResource(DugResource): - """ Execute a search """ - - """ System initiation. """ - def post(self): - """ - Execute the configured search. - - A natural language word or phrase is the input. - --- - tag: search - description: Search for a string - requestBody: - description: Search request - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Search' - responses: - '200': - description: Success - content: - text/plain: - schema: - type: string - example: "Nominal search" - '400': - description: Malformed message - content: - text/plain: - schema: - type: string - - """ - logger.debug(f"search:{json.dumps(request.json)}") - response = {} - try: - app.logger.info (f"search: {json.dumps(request.json, indent=2)}") - self.validate(request, component="Search") - boosted = request.json.pop('boosted', False) - - api_request = None - if boosted: - api_request = dug().search_nboost(**request.json) - else: - api_request = dug().search_concepts(**request.json) - - response = self.create_response( - result=api_request, - message=f"Search result") - except Exception as e: - response = self.create_response( - exception=e, - message=f"Failed to execute search {json.dumps(request.json, indent=2)}.") - return response - -class DugDumpConcept(DugResource): - """ Execute a search """ - - """ System initiation. """ - def post(self): - """ - Execute the search of all concepts. - - --- - tag: dump concepts - description: Get all concepts - requestBody: - description: Search request - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/Search' - responses: - '200': - description: Success - content: - text/plain: - schema: - type: string - example: "Nominal search" - '400': - description: Malformed message - content: - text/plain: - schema: - type: string - - """ - logger.debug(f"search:{json.dumps(request.json)}") - response = {} - try: - app.logger.info (f"search: {json.dumps(request.json, indent=2)}") - self.validate(request, component="Search") - # boosted = request.json.pop('boosted', False) - - api_request = dug().dump_concepts(**request.json) - - response = self.create_response( - result=api_request, - message=f"Search result") - except Exception as e: - response = self.create_response( - exception=e, - message=f"Failed to execute search {json.dumps(request.json, indent=2)}.") - return response - - -class DugSearchKGResource(DugResource): - """ Execute a search """ - - """ System initiation. """ - - def post(self): - """ - Execute the configured search. - - A natural language word or phrase is the input. - --- - tag: search_kg - description: Search for a string across knowledge graphs - requestBody: - description: Search request - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SearchKG' - responses: - '200': - description: Success - content: - text/plain: - schema: - type: string - example: "Nominal search" - '400': - description: Malformed message - content: - text/plain: - schema: - type: string - - """ - logger.debug(f"search_kg:{json.dumps(request.json)}") - response = {} - try: - app.logger.info(f"search_kg: {json.dumps(request.json, indent=2)}") - self.validate(request, component="Search") - response = self.create_response( - result=dug().search_kg(**request.json), - message=f"Search result") - except Exception as e: - response = self.create_response( - exception=e, - message=f"Failed to execute search {json.dumps(request.json, indent=2)}.") - return response - -class DugSearchVarResource(DugResource): - """ Execute a search """ - - """ System initiation. """ - - def post(self): - """ - Execute the configured search. - - A natural language word or phrase is the input. - --- - tag: search_kg - description: Search for a string across knowledge graphs - requestBody: - description: Search request - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SearchVar' - responses: - '200': - description: Success - content: - text/plain: - schema: - type: string - example: "Nominal search" - '400': - description: Malformed message - content: - text/plain: - schema: - type: string - - """ - logger.debug(f"search_kg:{json.dumps(request.json)}") - response = {} - try: - app.logger.info(f"search_var: {json.dumps(request.json, indent=2)}") - self.validate(request, component="Search") - response = self.create_response( - result=dug().search_variables(**request.json), - message=f"Search result") - except Exception as e: - response = self.create_response( - exception=e, - message=f"Failed to execute search {json.dumps(request.json, indent=2)}.") - return response - - -class DugAggDataType(DugResource): - """ Execute a search """ - - """ System initiation. """ - - def post(self): - logger.debug(f"data_type:{json.dumps(request.json)}") - response = {} - try: - app.logger.info(f"data_type: {json.dumps(request.json, indent=2)}") - self.validate(request, component="Search") - response = self.create_response( - result=dug().agg_data_type(**request.json), - message=f"Aggregate result") - except Exception as e: - response = self.create_response( - exception=e, - message=f"Failed to execute search {json.dumps(request.json, indent=2)}.") - return response - -""" Register endpoints. """ -api.add_resource(DugSearchResource, '/search') -api.add_resource(DugDumpConcept, '/dump_concepts') -api.add_resource(DugSearchKGResource, '/search_kg') -api.add_resource(DugSearchVarResource, '/search_var') -api.add_resource(DugAggDataType, '/agg_data_types') - -def main(args=None): - parser = argparse.ArgumentParser(description='Dug Search API') - parser.add_argument('-p', '--port', type=int, help='Port to run service on.', default=5551) - parser.add_argument('-d', '--debug', help="Debug log level.", default=False, action='store_true') - args = parser.parse_args(args) - - """ Configure """ - if args.debug: - debug = True - logging.basicConfig(level=logging.DEBUG) - logger.info(f"starting dug on port={args.port} with debug={args.debug}") - app.run(host='0.0.0.0', port=args.port, debug=args.debug, threaded=True) - -if __name__ == "__main__": - main() diff --git a/src/dug/conf/api-schema.yaml b/src/dug/conf/api-schema.yaml deleted file mode 100644 index f3b652a2..00000000 --- a/src/dug/conf/api-schema.yaml +++ /dev/null @@ -1,144 +0,0 @@ -openapi: 3.0.1 -info: - description: Exploratory bioinformatic datascience via software defined distributed systems. - version: 0.0.1 - title: - contact: - email: scox@renci.org - license: - name: Apache 2.0 - url: 'http://www.apache.org/licenses/LICENSE-2.0.html' -externalDocs: - description: Exploratory bioinformatic datascience via software defined distributed systems. - url: 'https://github.com/heliumplusdatastage/tycho' -tags: - - name: message - description: Request compute services. - externalDocs: - description: Documentation for the compute request. - url: 'https://github.com/heliumplusdatastage/tycho#request' -paths: - /system/start: - post: - summary: Compute service request. - description: '' - operationId: start - requestBody: - description: Compute service request. - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/System' - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Response' - '400': - description: Invalid status value - x-swagger-router-controller: swagger_server.controllers.query_controller - -components: - schemas: - Limits: - type: object - properties: - cpus: - type: string - example: "0.3" - description: Number of CPUs requested. May be a fractional value. - memory: - type: string - example: "512M" - description: Amount of memory to request for this container. - Port: - type: object - properties: - containerPort: - type: integer - example: 80 - description: Container port to expose - EnvironmentVariable: - type: object - properties: - name: - type: string - example: X - description: Name of an environment variable - value: - type: string - example: http://example.org - description: A string value. - Container: - type: object - properties: - name: - type: string - example: web-server - description: Name of the container to execute. - image: - type: string - example: nginx:1.9.1 - description: Name and version of a docker image to execute. - limits: - type: array - items: - $ref: '#/components/schemas/Limits' - example: - - cpus: "0.3" - memory: "512M" - command: - type: array - required: false - items: - type: string - env: - type: array - items: - $ref: '#/components/schemas/EnvironmentVariable' - ports: - type: array - items: - $ref: '#/components/schemas/Port' - System: - type: object - properties: - name: - type: string - example: some-stack - description: Description of the system provided and defined by this set of components. - containers: - type: array - items: - $ref: '#/components/schemas/Container' - Response: - type: object - properties: - status: - type: string - example: success | error - description: Status code denoting the outcome of the activity. - message: - type: string - example: Job succeeded. - description: Description of the result. - result: - type: object - DeleteRequest: - type: object - properties: - name: - type: string - example: test-app - description: Identifier of system to delete - StatusRequest: - type: object - properties: - name: - type: string - example: test-app - nullable: true - description: Identifier of system to list diff --git a/src/dug/core/async_search.py b/src/dug/core/async_search.py new file mode 100644 index 00000000..15c52043 --- /dev/null +++ b/src/dug/core/async_search.py @@ -0,0 +1,427 @@ +import json +import logging +from elasticsearch import AsyncElasticsearch +from elasticsearch.helpers import async_scan + +from dug.config import Config + +logger = logging.getLogger('dug') + + +class SearchException(Exception): + def __init__(self, message, details): + self.message = message + self.details = details + + +class Search: + """ Search - + 1. Lexical fuzziness; (a) misspellings - a function of elastic. + 2. Fuzzy ontologically; + (a) expand based on core queries + * phenotype->study + * phenotype->disease->study + * disease->study + * disease->phenotype->study + """ + + def __init__(self, cfg: Config, indices=None): + + if indices is None: + indices = ['concepts_index', 'variables_index', 'kg_index'] + + self._cfg = cfg + logger.debug(f"Connecting to elasticsearch host: {self._cfg.elastic_host} at port: {self._cfg.elastic_port}") + + self.indices = indices + self.hosts = [{'host': self._cfg.elastic_host, 'port': self._cfg.elastic_port}] + + logger.debug(f"Authenticating as user {self._cfg.elastic_username} to host:{self.hosts}") + + self.es = AsyncElasticsearch(hosts=self.hosts, + http_auth=(self._cfg.elastic_username, self._cfg.elastic_password)) + + async def dump_concepts(self, index, query={}, size=None, fuzziness=1, prefix_length=3): + """ + Get everything from concept index + """ + query = { + "match_all" : {} + } + body = {"query": query} + await self.es.ping() + total_items = await self.es.count(body=body, index=index) + counter = 0 + all_docs = [] + async for doc in async_scan( + client=self.es, + query=body, + index=index + ): + if counter == size and size != 0: + break + counter += 1 + all_docs.append(doc) + return { + "status": "success", + "result": { + "hits": { + "hits": all_docs + }, + "total_items": total_items + }, + "message": "Search result" + } + + async def agg_data_type(self): + aggs = { + "data_type": { + "terms": { + "field": "data_type.keyword", + } + } + } + + body = {'aggs': aggs} + results = await self.es.search( + index="variables_index", + body=body + ) + data_type_list = [data_type['key'] for data_type in results['aggregations']['data_type']['buckets']] + results.update({'data type list': data_type_list}) + return data_type_list + + async def search_concepts(self, query, offset=0, size=None, fuzziness=1, prefix_length=3): + """ + Changed to a long boolean match query to optimize search results + """ + query = { + "bool": { + "should": [ + { + "match_phrase": { + "name": { + "query": query, + "boost": 10 + } + } + }, + { + "match_phrase": { + "description": { + "query": query, + "boost": 6 + } + } + }, + { + "match_phrase": { + "search_terms": { + "query": query, + "boost": 8 + } + } + }, + { + "match": { + "name": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "operator": "and", + "boost": 4 + } + } + }, + { + "match": { + "search_terms": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "operator": "and", + "boost": 5 + } + } + }, + { + "match": { + "description": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "operator": "and", + "boost": 3 + } + } + }, + { + "match": { + "description": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "boost": 2 + } + } + }, + { + "match": { + "search_terms": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "boost": 1 + } + } + }, + { + "match": { + "optional_terms": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length + } + } + } + ] + } + } + body = json.dumps({'query': query}) + total_items = await self.es.count(body=body, index="concepts_index") + search_results = await self.es.search( + index="concepts_index", + body=body, + filter_path=['hits.hits._id', 'hits.hits._type', 'hits.hits._source', 'hits.hits._score'], + from_=offset, + size=size + ) + search_results.update({'total_items': total_items['count']}) + return search_results + + async def search_variables(self, concept="", query="", size=None, data_type=None, offset=0, fuzziness=1, + prefix_length=3): + """ + In variable seach, the concept MUST match one of the indentifiers in the list + The query can match search_terms (hence, "should") for ranking. + + Results Return + The search result is returned in JSON format {collection_id:[elements]} + + Filter + If a data_type is passed in, the result will be filtered to only contain + the passed-in data type. + """ + query = { + 'bool': { + 'should': { + "match": { + "identifiers": concept + } + }, + 'should': [ + { + "match_phrase": { + "element_name": { + "query": query, + "boost": 10 + } + } + }, + { + "match_phrase": { + "element_desc": { + "query": query, + "boost": 6 + } + } + }, + { + "match_phrase": { + "search_terms": { + "query": query, + "boost": 8 + } + } + }, + { + "match": { + "element_name": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "operator": "and", + "boost": 4 + } + } + }, + { + "match": { + "search_terms": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "operator": "and", + "boost": 5 + } + } + }, + { + "match": { + "element_desc": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "operator": "and", + "boost": 3 + } + } + }, + { + "match": { + "element_desc": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "boost": 2 + } + } + }, + { + "match": { + "element_name": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "boost": 2 + } + } + }, + { + "match": { + "search_terms": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length, + "boost": 1 + } + } + }, + { + "match": { + "optional_terms": { + "query": query, + "fuzziness": fuzziness, + "prefix_length": prefix_length + } + } + } + ] + } + } + + if concept: + query['bool']['must'] = { + "match": { + "identifiers": concept + } + } + + body = json.dumps({'query': query}) + total_items = await self.es.count(body=body, index="variables_index") + search_results = await self.es.search( + index="variables_index", + body=body, + filter_path=['hits.hits._id', 'hits.hits._type', 'hits.hits._source', 'hits.hits._score'], + from_=offset, + size=size + ) + + # Reformat Results + new_results = {} + if not search_results: + # we don't want to error on a search not found + new_results.update({'total_items': total_items['count']}) + return new_results + + for elem in search_results['hits']['hits']: + elem_s = elem['_source'] + elem_type = elem_s['data_type'] + if elem_type not in new_results: + new_results[elem_type] = {} + + elem_id = elem_s['element_id'] + coll_id = elem_s['collection_id'] + elem_info = { + "description": elem_s['element_desc'], + "e_link": elem_s['element_action'], + "id": elem_id, + "name": elem_s['element_name'], + "score": round(elem['_score'], 6) + } + + # Case: collection not in dictionary for given data_type + if coll_id not in new_results[elem_type]: + # initialize document + doc = {} + + # add information + doc['c_id'] = coll_id + doc['c_link'] = elem_s['collection_action'] + doc['c_name'] = elem_s['collection_name'] + doc['elements'] = [elem_info] + + # save document + new_results[elem_type][coll_id] = doc + + # Case: collection already in dictionary for given element_type; append elem_info. Assumes no duplicate elements + else: + new_results[elem_type][coll_id]['elements'].append(elem_info) + + # Flatten dicts to list + for i in new_results: + new_results[i] = list(new_results[i].values()) + + # Return results + if bool(data_type): + if data_type in new_results: + new_results = new_results[data_type] + else: + new_results = {} + return new_results + + async def search_kg(self, unique_id, query, offset=0, size=None, fuzziness=1, prefix_length=3): + """ + In knowledge graph search the concept MUST match the unique ID + The query MUST match search_targets. The updated query allows for + fuzzy matching and for the default OR behavior for the query. + """ + query = { + "bool": { + "must": [ + {"term": { + "concept_id.keyword": unique_id + } + }, + {'query_string': { + "query": query, + "fuzziness": fuzziness, + "fuzzy_prefix_length": prefix_length, + "default_field": "search_targets" + } + } + ] + } + } + body = json.dumps({'query': query}) + total_items = await self.es.count(body=body, index="kg_index") + search_results = await self.es.search( + index="kg_index", + body=body, + filter_path=['hits.hits._id', 'hits.hits._type', 'hits.hits._source'], + from_=offset, + size=size + ) + search_results.update({'total_items': total_items['count']}) + return search_results + diff --git a/src/dug/core/search.py b/src/dug/core/search.py index 28fc7b0c..26a7035f 100644 --- a/src/dug/core/search.py +++ b/src/dug/core/search.py @@ -1,3 +1,9 @@ +### +# Deprication Notice: +# New Changes to search and indexing should be made in the async flavor of dug. +# see : ./async_search.py +### + import json import logging diff --git a/src/dug/server.py b/src/dug/server.py new file mode 100644 index 00000000..00a477d9 --- /dev/null +++ b/src/dug/server.py @@ -0,0 +1,104 @@ +import logging +import os +import uvicorn + +from fastapi import FastAPI +from dug.config import Config +from dug.core.async_search import Search +from pydantic import BaseModel +import asyncio + +logger = logging.getLogger (__name__) + +APP = FastAPI( + title="Dug Search API", + root_path=os.environ.get("ROOT_PATH", "/"), +) + + +class GetFromIndex(BaseModel): + index: str = "concepts_index" + size: int = 0 + + +class SearchConceptQuery(BaseModel): + query: str + index: str = "concepts_index" + offset: int = 0 + size: int = 20 + +class SearchVariablesQuery(BaseModel): + query: str + index: str = "variables_index" + concept: str = "" + offset: int = 0 + size: int = 1000 + +class SearchKgQuery(BaseModel): + query: str + unique_id: str + index: str = "kg_index" + size:int = 100 + + +search = Search(Config.from_env()) + + +@APP.on_event("shutdown") +def shutdown_event(): + asyncio.run(search.es.close()) + + +@APP.post('/dump_concepts') +async def dump_concepts(request: GetFromIndex): + return { + "message": "Dump result", + "result": await search.dump_concepts(**request.dict()), + "status": "success" + } + + +@APP.get('/agg_data_types') +async def agg_data_types(): + return { + "message": "Dump result", + "result": await search.agg_data_type(), + "status": "success" + } + + +@APP.post('/search') +async def search_concepts(search_query: SearchConceptQuery): + return { + "message": "Search result", + # Although index in provided by the query we will keep it around for backward compatibility, but + # search concepts should always search against "concepts_index" + "result": await search.search_concepts(**search_query.dict(exclude={"index"})), + "status": "success" + } + + +@APP.post('/search_kg') +async def search_kg(search_query: SearchKgQuery): + return { + "message": "Search result", + # Although index in provided by the query we will keep it around for backward compatibility, but + # search concepts should always search against "kg_index" + "result": await search.search_kg(**search_query.dict(exclude={"index"})), + "status": "success" + } + + +@APP.post('/search_var') +async def search_var(search_query: SearchVariablesQuery): + return { + "message": "Search result", + # Although index in provided by the query we will keep it around for backward compatibility, but + # search concepts should always search against "variables_index" + "result": await search.search_variables(**search_query.dict(exclude={"index"})), + "status": "success" + } + + +if __name__ == '__main__': + uvicorn.run(APP) \ No newline at end of file