Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Origin/user friendly searching #14

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/deconfliction_service/node_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django_neomodel import DjangoNode
from neomodel import db
import numpy as np
import torch
#import torch
from sentence_transformers import SentenceTransformer, util
import logging
from core.constants import MODEL_VECTOR_DIMENSION
Expand Down
12 changes: 12 additions & 0 deletions app/uid/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ class Meta:
model = LCVTerm
#fields = ['uid', 'term', 'echelon_level']
fields = ['term', 'echelon_level'] # UID is self Generated

# Search Forms
class SearchForm(forms.Form):
search_term = forms.CharField(max_length=255, required=True, label="Search Term")
search_type = forms.ChoiceField(choices=[
('general', 'General Search'),
('alias', 'Search by Alias'),
('definition', 'Search by Definition'),
('context', 'Search by Context'),
], required=True, label="Search Type"
)
context = forms.CharField(label='Context', required=False, max_length=255)
111 changes: 111 additions & 0 deletions app/uid/templates/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
}
.container {
width: 80%;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
label {
font-size: 16px;
margin-bottom: 8px;
display: block;
}
input[type="text"], select, button {
padding: 10px;
font-size: 16px;
width: 100%;
max-width: 400px;
margin-top: 5px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #45a049;
}
.results-list {
margin-top: 30px;
}
.result-item {
background-color: #fff;
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 10px;
border-radius: 5px;
}
.error-message {
color: red;
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<h1>Search CCV/LCV Definitions</h1>

<form method="POST">
{% csrf_token %}
<div class="form-group">
<label for="search_term">Search Term:</label>
<input type="text" name="search_term" id="search_term" placeholder="Enter search term" required>
</div>

<div class="form-group">
<label for="search_type">Search By:</label>
<select name="search_type" id="search_type">
<option value="alias">Alias</option>
<option value="definition">Definition</option>
<option value="context">Context</option>
<option value="general">General</option>
</select>
</div>

<div class="form-group">
<button type="submit">Search</button>
</div>
</form>

<!-- Display search results -->
{% if results %}
<div class="results-list">
<h3>Search Results</h3>
{% for result in results %}
<div class="result-item">
<strong>LCVID:</strong> {{ result.LCVID }}<br>
<strong>Alias:</strong> {{ result.Alias }}<br>
<strong>Definition:</strong> {{ result.Definition }}<br>
<strong>Context:</strong> {{ result.Context|default:"No context" }}<br>
</div>
{% endfor %}
</div>
{% endif %}

{% if error %}
<p class="error-message">{{ error }}</p>
{% endif %}
</div>
</body>
</html>

2 changes: 2 additions & 0 deletions app/uid/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
path('export/<str:uid>/', export_to_postman, name='export_to_postman'),
path('report/<str:echelon_level>/', generate_report, name='generate_report'),
path('api/uid-repo/', UIDRepoViewSet.as_view({'get': 'list'}), name='uid-repo'),
path('search/', views.search, name='search'), # For the search endpoint
#path('search/', views.search_view, name='search'), # Maps the search endpoint
]
153 changes: 133 additions & 20 deletions app/uid/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,81 @@
from rest_framework import viewsets
from rest_framework.response import Response

import os
from .forms import SearchForm
import requests
import urllib.parse

# Neo4j connection details
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME', 'neo4j') # Default username if env var not set
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD', 'password') # Default password if env var not set
NEO4J_HOST = os.getenv("NEO4J_HOST", "localhost") # Default to localhost if not set
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole block shoul be removed -- get the connection string from settings.

from django.conf import settings

connection_url = settings.DATABASE_URL

NEO4J_PORT = os.getenv("NEO4J_PORT", "7687") # Default to 7687 if not set

# URL-encode the password if it contains special characters
encoded_password = urllib.parse.quote(NEO4J_PASSWORD)

# Construct the correct Neo4j connection string
connection_url = f"bolt://{NEO4J_USERNAME}:{encoded_password}@{NEO4J_HOST}:{NEO4J_PORT}"

# Set the connection using Neomodel's `db.set_connection` method
db.set_connection(connection_url)
logger.info(f"Connected to Neo4j at: {connection_url}") # Debug logs to check Neo4J DB connection.

# Cypher Queries
SEARCH_BY_ALIAS = """
WITH toLower($search_term) as search_term
MATCH (a:NeoAlias)
WHERE toLower(a.alias) CONTAINS search_term
MATCH (a)-[:POINTS_TO]->(term:NeoTerm)
OPTIONAL MATCH (term)-[:POINTS_TO]->(def:NeoDefinition)
OPTIONAL MATCH (ctx:NeoContext)-[:IS_A]->(term)
RETURN term.uid as LCVID, a.alias as Alias, def.definition as Definition, ctx.context as Context
LIMIT 100
"""

SEARCH_BY_DEFINITION = """
WITH toLower($search_term) as search_term
MATCH (def:NeoDefinition)
WHERE toLower(def.definition) CONTAINS search_term
MATCH (term:NeoTerm)-[:POINTS_TO]->(def)
OPTIONAL MATCH (a:NeoAlias)-[:POINTS_TO]->(term)
OPTIONAL MATCH (ctx:NeoContext)-[:IS_A]->(term)
RETURN term.uid as LCVID, a.alias as Alias, def.definition as Definition, ctx.context as Context
LIMIT 100
"""

SEARCH_BY_CONTEXT = """
WITH toLower($search_term) as search_term
MATCH (ctx:NeoContext)
WHERE toLower(ctx.context) CONTAINS search_term
MATCH (ctx)-[:IS_A]->(term:NeoTerm)
OPTIONAL MATCH (term)-[:POINTS_TO]->(def:NeoDefinition)
OPTIONAL MATCH (a:NeoAlias)-[:POINTS_TO]->(term)
RETURN term.uid as LCVID, a.alias as Alias, def.definition as Definition, ctx.context as Context
LIMIT 100
"""

GENERAL_GRAPH_SEARCH = """
WITH toLower($search_term) as search_term
MATCH (n)
WHERE (n:NeoAlias OR n:NeoDefinition OR n:NeoContext)
AND (
(n:NeoAlias AND toLower(n.alias) CONTAINS search_term) OR
(n:NeoDefinition AND toLower(n.definition) CONTAINS search_term) OR
(n:NeoContext AND toLower(n.context) CONTAINS search_term)
)
WITH n
CALL {
WITH n
MATCH path = (n)-[*1..2]-(connected)
RETURN path
}
RETURN * LIMIT 100
"""

# Globally Declare variable
MAX_CHILDREN = 2**32 -1

# Set up logging to capture errors and important information
logger = logging.getLogger(__name__)
Expand All @@ -21,7 +96,63 @@
logger.error(f"Failed to initialize UIDGenerator: {e}")
uid_generator = None # Handle initialization failure appropriately

MAX_CHILDREN = 2**32 -1
def execute_neo4j_query(query, params):
query_str = query
try:
logger.info(f"Executing query: {query} with params: {params}")
results, meta = db.cypher_query(query_str, params)
return results
except Exception as e:
logger.error(f"Error executing Neo4j query: {e}")
return None

# Django view for search functionality
def search(request):
results = []
if request.method == 'POST':
form = SearchForm(request.POST)
if form.is_valid():
search_term = form.cleaned_data['search_term']
search_type = form.cleaned_data['search_type']

# Log form data for debugging
logger.info(f"Search form data: search_term={search_term}, search_type={search_type}")

# Determine which query to use based on search type
if search_type == 'alias':
query = SEARCH_BY_ALIAS
elif search_type == 'definition':
query = SEARCH_BY_DEFINITION
elif search_type == 'context':
query = SEARCH_BY_CONTEXT
else:
query = GENERAL_GRAPH_SEARCH # For 'general' search

# Log the query and params being sent to Neo4j
logger.info(f"Executing query: {query} with params: {{'search_term': {search_term}}}")

# Execute the query
results_data = execute_neo4j_query(query, {"search_term": search_term})

if results_data:
logger.info(f"Raw results data: {results_data}")
results = [
{
"LCVID": record['row'][0],
"Alias": record['row'][1],
"Definition": record['row'][2],
"Context": record['row'][3] if record['row'][3] else "No context" # Handle missing context
}
for record in results_data['results'][0]['data']
]
else:
logger.info("No results found.")
results = [{'error': 'No results found or error querying Neo4j.'}]

else:
form = SearchForm()

return render(request, 'search.html', {'form': form, 'results': results})

# Create your views here.
def generate_uid_node(request: HttpRequest):
Expand Down Expand Up @@ -58,24 +189,6 @@ def generate_uid_node(request: HttpRequest):

return HttpResponse("{ 'uid': '" + str(local_uid) + "' }", content_type='application/json')

#Potential code to retrieve parent and child nodes using the upstream and downstream capabilities
#def get_upstream_providers(request, uid):
#try:
# lcv_term = LCVTerm.nodes.get(uid=uid)
# upstream_providers = lcv_term.get_upstream()
# upstream_uids = [p.uid for p in upstream_providers]
# return JsonResponse({'upstream_uids': upstream_uids})
# except LCVTerm.DoesNotExist:
# return JsonResponse({'error': 'LCVTerm not found'}, status=404)

#def get_downstream_lcv_terms(request, uid):
#try:
#provider = Provider.nodes.get(uid=uid)
# downstream_lcv_terms = provider.get_downstream()
# downstream_uids = [l.uid for l in downstream_lcv_terms]
# return JsonResponse({'downstream_uids': downstream_uids})
#except Provider.DoesNotExist:
# return JsonResponse({'error': 'Provider not found'}, status=404)

# Provider and LCVTerm (Otherwise alternative Parent and child) Now with collision detection on both.
def create_provider(request):
Expand Down Expand Up @@ -156,4 +269,4 @@ def export_to_postman(request, uid):
except LanguageSet.DoesNotExist:
return JsonResponse({'error': 'UID not found'}, status=404)

return JsonResponse(data)
return JsonResponse(data)
15 changes: 4 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
- '3310:3306'
environment:
MYSQL_DATABASE: "${DB_NAME}"
MYSQL_USER: "${DB_USER}"
MYSQL_USER: "${DB_USER}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
# MYSQL_HOST: ''
Expand All @@ -16,6 +16,7 @@ services:

neo4j:
image: neo4j:5.15.0
restart: always
ports:
- '7687:7687'
- '7474:7474'
Expand Down Expand Up @@ -47,25 +48,17 @@ services:
# BAD_HOST: "${BAD_HOST}"
# OVERIDE_HOST: "${OVERIDE_HOST}"
# STRATEGY: "${STRATEGY}"
SENTENCE_TRANSFORMERS_HOME: "${SENTENCE_TRANSFORMERS_HOME}"
HF_HOME: "${HF_HOME}"
TRANSFORMERS_CACHE: "${TRANSFORMERS_CACHE}"
HF_DATASETS_CACHE: "${HF_DATASETS_CACHE}"
#NEO4J_URL: "bolt://neo4j:password@neo4j:7687"
volumes:
- ./app:/opt/app/openlxp-xss
- huggingface_data:/opt/app/huggingface
depends_on:
- neo4j
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The neo4j dependency should not be removed here, as the app container does definitely depend on it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh, I did not touch the .yml file for the manual search function.

- db-xss
networks:
- openlxp
volumes:
es_data:
driver: local
data01:
driver: local
huggingface_data:
driver: local
networks:
openlxp:
driver: bridge
# Test