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
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# =============================================================================
|
||||
# Django API image.
|
||||
#
|
||||
# Two stages: wheels are built with a compiler present, the runtime image gets
|
||||
# only the compiled artefacts. The result is small, has no build toolchain to
|
||||
# exploit, and runs as an unprivileged user on a read-only-friendly layout.
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 1 - build wheels
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM python:3.12-slim-bookworm AS builder
|
||||
|
||||
ENV PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y build-essential libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The build context is the repository root so the image can also carry the
|
||||
# dataset; paths below are therefore prefixed with the directory name.
|
||||
WORKDIR /build
|
||||
COPY backend/requirements/ requirements/
|
||||
RUN pip wheel --wheel-dir /wheels -r requirements/base.txt
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 2 - runtime
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM python:3.12-slim-bookworm AS runtime
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
DJANGO_SETTINGS_MODULE=config.settings.prod \
|
||||
PATH="/home/app/.local/bin:${PATH}"
|
||||
|
||||
# libpq5 is the only runtime library psycopg needs; curl backs the healthcheck.
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y libpq5 curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --gid 10001 app \
|
||||
&& useradd --uid 10001 --gid app --create-home --shell /usr/sbin/nologin app
|
||||
|
||||
COPY --from=builder /wheels /wheels
|
||||
COPY backend/requirements/ /tmp/requirements/
|
||||
RUN pip install --no-index --find-links=/wheels -r /tmp/requirements/base.txt \
|
||||
&& rm -rf /wheels /tmp/requirements
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=app:app backend/ /app/
|
||||
COPY --chown=app:app data/ /data/
|
||||
|
||||
# Static assets are only needed for the admin and the API docs, but baking them
|
||||
# in keeps the container stateless.
|
||||
RUN DJANGO_SECRET_KEY=build-only-not-a-real-secret-000000000000000000000000000000000000 \
|
||||
DATABASE_URL=postgres://build:build@localhost:5432/build \
|
||||
DJANGO_SETTINGS_MODULE=config.settings.dev \
|
||||
python manage.py collectstatic --noinput --clear \
|
||||
&& chmod +x /app/entrypoint.sh
|
||||
|
||||
USER app
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS http://localhost:8000/api/v1/health/live/ || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["gunicorn", "config.wsgi:application", "--config", "/app/gunicorn.conf.py"]
|
||||
Reference in New Issue
Block a user