Files
cyberyan_linkedin_search/backend/tests/test_query_builder.py
T
mobiniqow a464e550c9 feat: LinkedIn profile search engine (Django + React + Elasticsearch + ELK)
Full-stack implementation of the Cyberyan full-stack trial task.

Backend (Django 5 / DRF)
- Normalised PostgreSQL model (Profile, Company, School, Skill, Experience,
  Education) as the source of truth, with GIN + trigram indexes.
- Self-healing CSV reader: the supplied export carries an injected watermark
  column, missing columns, rows split by broken quoting and a duplicated
  header. A signature-scored Needleman-Wunsch alignment reconstructs 284 clean
  records out of 336 physical rows; only 10 rows parse correctly without it.
- Pluggable search layer (Strategy): Elasticsearch by default, PostgreSQL
  full-text search as a fallback selected by a single environment variable.
- Keyword search with per-field boosts, fuzziness and phrase boosting, six
  faceted filters (OR within a filter, AND across filters), an experience
  range, four sort orders, highlighting and facet aggregations.
- JWT auth with rotation + blacklist, three roles, PII masking, scoped rate
  limits, a single error envelope, OpenAPI schema and health probes.

Frontend (React 18 / TypeScript / Vite)
- Debounced search with type-ahead, faceted filter panel, result cards,
  profile drawer, URL-synced search state, automatic token refresh.
- Highlight snippets rendered without dangerouslySetInnerHTML.

Infrastructure
- Hardened multi-stage images (non-root, read-only rootfs, healthchecks).
- docker-compose for the full stack plus dev and production overlays.
- ELK: structured JSON logs shipped asynchronously to Logstash, routed per
  event type, with a composable index template and a Kibana setup script.

Quality
- 61 backend tests (85% coverage), ruff/black/bandit/mypy, ESLint + tsc,
  GitHub Actions running lint, tests and both image builds.
- Persian documentation: README plus architecture, search, API, data,
  security and operations guides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnFo6fNaHCTAkJbfcdkqtG
2026-09-03 08:13:50 +00:00

77 lines
2.9 KiB
Python

"""
Unit tests for the Elasticsearch query builder.
``build_query`` is a pure function, so the ranking and filtering rules can be
asserted without a running cluster - the fastest possible feedback loop for the
part of the system that is easiest to get subtly wrong.
"""
from __future__ import annotations
from apps.search.backends.elastic import build_query
from apps.search.types import SORT_EXPERIENCE_DESC, SORT_NAME, SearchRequest
def _bool(request: SearchRequest) -> dict:
return build_query(request)["query"]["bool"]
def test_empty_query_matches_everything() -> None:
clause = _bool(SearchRequest())
assert clause["must"] == [{"match_all": {}}]
assert clause["filter"] == []
def test_keyword_query_uses_fuzzy_multi_match_and_phrase_boost() -> None:
clause = _bool(SearchRequest(q="python developer"))
multi_match = clause["must"][0]["multi_match"]
assert multi_match["query"] == "python developer"
assert multi_match["fuzziness"] == "AUTO"
assert any(field.startswith("full_name^") for field in multi_match["fields"])
# The phrase clause only boosts; it must never be required.
assert clause["should"][0]["multi_match"]["type"] == "phrase"
def test_filters_are_terms_clauses_in_filter_context() -> None:
clause = _bool(
SearchRequest(skills=("python", "django"), countries=("germany",), seniority=("senior",))
)
assert {"terms": {"skills.keyword": ["python", "django"]}} in clause["filter"]
assert {"terms": {"location_country": ["germany"]}} in clause["filter"]
assert {"terms": {"seniority_levels": ["senior"]}} in clause["filter"]
# Filters must not leak into the scoring clauses.
assert clause["must"] == [{"match_all": {}}]
def test_experience_range_is_translated_to_a_range_filter() -> None:
clause = _bool(SearchRequest(min_experience=5, max_experience=10))
assert {"range": {"years_experience": {"gte": 5, "lte": 10}}} in clause["filter"]
open_ended = _bool(SearchRequest(min_experience=5))
assert {"range": {"years_experience": {"gte": 5}}} in open_ended["filter"]
def test_pagination_maps_to_from_and_size() -> None:
body = build_query(SearchRequest(page=3, page_size=25))
assert body["from"] == 50
assert body["size"] == 25
assert body["track_total_hits"] is True
def test_sort_options() -> None:
assert build_query(SearchRequest(sort=SORT_NAME))["sort"][0] == {"full_name.keyword": "asc"}
assert build_query(SearchRequest(sort=SORT_EXPERIENCE_DESC))["sort"][0] == {
"years_experience": {"order": "desc", "missing": "_last"}
}
assert build_query(SearchRequest())["sort"][0] == "_score"
def test_highlight_only_requested_with_a_keyword() -> None:
assert "highlight" not in build_query(SearchRequest())
assert "highlight" in build_query(SearchRequest(q="python"))
def test_facets_can_be_switched_off() -> None:
assert "aggs" in build_query(SearchRequest())
assert "aggs" not in build_query(SearchRequest(include_facets=False))