start project
CI / Backend (lint, types, tests) (push) Has been cancelled
CI / Frontend (lint, types, build) (push) Has been cancelled
CI / Docker images build (push) Has been cancelled

This commit is contained in:
2026-09-04 12:52:21 +03:30
parent a464e550c9
commit 212074c5f5
40 changed files with 2804 additions and 164 deletions
+89 -53
View File
@@ -42,14 +42,26 @@ _TOKEN_RE = re.compile(r"[\w']+", re.UNICODE)
_FACET_LIMIT = 25
_MIN_TRIGRAM_SIMILARITY = 0.3
#: Maps a facet (as returned to the frontend) to the request attribute that
#: selects values *within that same facet*. Used to exclude a facet's own
#: filter from its own count - see ``_facets``.
_FACET_TO_FILTER_ATTR = {
"skills": "skills",
"job_title_role": "roles",
"seniority": "seniority",
"country": "countries",
"industry": "industries",
"company": "companies",
}
class PostgresBackend(SearchBackend):
name = "postgres"
def search(self, request: SearchRequest) -> SearchResult:
started = time.perf_counter()
queryset = self._apply_filters(Profile.objects.all(), request)
queryset = self._apply_keyword(queryset, request.q)
keyword_queryset = self._apply_keyword(Profile.objects.all(), request.q)
queryset = self._apply_filters(keyword_queryset, request)
total = queryset.count()
page = (
@@ -60,7 +72,7 @@ class PostgresBackend(SearchBackend):
]
)
hits = [self._to_hit(profile, request.q) for profile in page]
facets = self._facets(queryset) if request.include_facets else {}
facets = self._facets(keyword_queryset, request) if request.include_facets else {}
return SearchResult(
total=total,
@@ -87,23 +99,35 @@ class PostgresBackend(SearchBackend):
# -- Query construction -------------------------------------------------
@staticmethod
def _apply_filters(queryset: QuerySet[Profile], request: SearchRequest) -> QuerySet[Profile]:
if request.skills:
def _apply_filters(
queryset: QuerySet[Profile], request: SearchRequest, *, exclude: str | None = None
) -> QuerySet[Profile]:
"""
Apply every active filter except ``exclude``.
``exclude`` is how a facet's own count query avoids narrowing itself:
pass e.g. ``exclude="roles"`` when computing the job-family facet so
ticking "Engineering" does not make "Human resources" disappear from
that same list. The main hits queryset calls this with no exclusion.
"""
if request.skills and exclude != "skills":
# Every selected skill widens the result set (OR), mirroring the
# ``terms`` filter used by the Elasticsearch backend.
queryset = queryset.filter(
skills__slug__in=[Skill.make_slug(s) for s in request.skills]
)
if request.roles:
if request.roles and exclude != "roles":
queryset = queryset.filter(job_title_role__in=request.roles)
if request.seniority:
if request.seniority and exclude != "seniority":
queryset = queryset.filter(seniority_levels__overlap=list(request.seniority))
if request.countries:
if request.countries and exclude != "countries":
queryset = queryset.filter(location_country__in=request.countries)
if request.industries:
if request.industries and exclude != "industries":
queryset = queryset.filter(industry__in=request.industries)
if request.companies:
if request.companies and exclude != "companies":
queryset = queryset.filter(current_company__name__in=request.companies)
# The experience range is not a facet with its own checkboxes, so it
# always applies - it is never the thing being excluded.
if request.min_experience is not None:
queryset = queryset.filter(years_experience__gte=request.min_experience)
if request.max_experience is not None:
@@ -152,53 +176,65 @@ class PostgresBackend(SearchBackend):
return queryset.order_by("-rank", "full_name", "id")
# -- Facets -------------------------------------------------------------
def _facets(self, queryset: QuerySet[Profile]) -> dict[str, list[FacetBucket]]:
def _facets(
self, keyword_queryset: QuerySet[Profile], request: SearchRequest
) -> dict[str, list[FacetBucket]]:
"""
One count per facet, each computed on a queryset that excludes that
facet's *own* filter.
This is what keeps a multi-select facet fully populated once one of
its values is ticked: the job-family facet must still list "Human
resources" after "Engineering" is selected, while every *other* facet
(country, seniority, ...) does narrow to the engineering-only result
set. Six independent queries instead of one is a deliberate
trade-off - see the module docstring on why this backend does not aim
to match Elasticsearch's scale, only its contract.
"""
return {
name: self._facet_counts(
name, self._apply_filters(keyword_queryset, request, exclude=attribute)
)
for name, attribute in _FACET_TO_FILTER_ATTR.items()
}
@staticmethod
def _facet_counts(facet_name: str, queryset: QuerySet[Profile]) -> list[FacetBucket]:
ids = list(queryset.values_list("id", flat=True))
if not ids:
return {
name: []
for name in (
"skills",
"job_title_role",
"seniority",
"country",
"industry",
"company",
return []
if facet_name == "skills":
counter = Counter(
Skill.objects.filter(profile_skills__profile_id__in=ids).values_list(
"name", flat=True
)
}
)
elif facet_name == "seniority":
counter = Counter()
for levels in Profile.objects.filter(id__in=ids).values_list(
"seniority_levels", flat=True
):
for level in levels or []:
counter[level] += 1
else:
column = {
"job_title_role": "job_title_role",
"country": "location_country",
"industry": "industry",
"company": "current_company__name",
}[facet_name]
counter = Counter(
value or "" for value in Profile.objects.filter(id__in=ids).values_list(
column, flat=True
)
)
skills = Skill.objects.filter(profile_skills__profile_id__in=ids).values_list(
"name", flat=True
)
rows = Profile.objects.filter(id__in=ids).values(
"job_title_role", "location_country", "industry", "current_company__name",
"seniority_levels",
) # fmt: skip
counters: dict[str, Counter] = {
"skills": Counter(skills),
"job_title_role": Counter(),
"seniority": Counter(),
"country": Counter(),
"industry": Counter(),
"company": Counter(),
}
for row in rows:
counters["job_title_role"][row["job_title_role"]] += 1
counters["country"][row["location_country"]] += 1
counters["industry"][row["industry"]] += 1
counters["company"][row["current_company__name"] or ""] += 1
for level in row["seniority_levels"] or []:
counters["seniority"][level] += 1
return {
name: [
FacetBucket(key=key, count=count)
for key, count in counter.most_common(_FACET_LIMIT)
if key
]
for name, counter in counters.items()
}
return [
FacetBucket(key=key, count=count)
for key, count in counter.most_common(_FACET_LIMIT)
if key
]
# -- Result mapping -----------------------------------------------------
@classmethod