How the itrstats tax assistant works: one query, every layer

This post walks through how the itrstats (https://itrstats.in) tax assistant handles a single compound user question, end to end through every layer of the backend.

A user types this in:

What’s tax on ₹15 lakh in new regime, what percentile am I in, and is the marginal relief rule relevant here?

The response that comes back:

  • In the new regime, tax on ₹15 lakh is ₹97,500 for FY 2025-26.
  • Under the old regime, the same income would be ₹2,57,400, so the new regime is cheaper by ₹1,59,900.
  • You are in roughly the top 17.42% of Indian taxpayers, with about 82.58% earning less.
  • Marginal relief is not relevant here because it applies around the ₹12 lakh 87A rebate threshold and surcharge thresholds, not at ₹15 lakh.

Behind that response, three tools fired, the model made two passes, and a composer did a final validation strip before anything left the server. The whole thing finishes in about four seconds. The model did not compute a single number in the entire trace. It picked tools, narrated the result, and was kept on the rails by a Pydantic-enforced output schema. This post follows that one query through every layer of the system.

┌──────┐   POST /v1/assistant/query   ┌──────────┐   ┌─────────────┐
│ user │ ───────────────────────────▶ │  route   │──▶│   action    │
└──────┘                              └──────────┘   └──────┬──────┘
   ▲                                                        │
   │                                                        ▼
   │                                              ┌─────────────────┐
   │                                              │   agent loop    │
   │                                              │  (MAX_HOPS=3)   │
   │                                              └────────┬────────┘
   │                                                       │
   │                              ┌────────────────────────┼────────────────────────┐
   │                              ▼                        ▼                        ▼
   │                  ┌────────────────────┐   ┌─────────────────────┐   ┌──────────────────┐
   │                  │ compute_income_tax │   │ compute_income_     │   │ retrieve_        │
   │                  │ (pure Python)      │   │ percentile (Python) │   │ knowledge (RAG)  │
   │                  └─────────┬──────────┘   └──────────┬──────────┘   └────────┬─────────┘
   │                            │                         │                       │
   │                            └─────────────────────────┴───────────────────────┘
   │                                                      │
   │                                                      ▼
   │                                            ┌──────────────────┐
   │                                            │ agent (hop 2):   │
   │                                            │ narrate answer   │
   │                                            └────────┬─────────┘
   │                                                     ▼
   │                                            ┌──────────────────┐
   └──── public response  ◀──────────────────── │    composer      │
                                                └──────────────────┘

Hop zero: the request arrives

The POST lands at /v1/assistant/query. The route is intentionally thin: validate, rate-limit, call one action, return. Orchestration lives in the action layer, which does the boring-but-necessary plumbing before the agent ever runs:

  • Resolves a conversation ID, generating a new one if needed.
  • Persists the request row to Postgres before the model is called, so a downstream crash still leaves a record of what was asked.
  • Loads recent conversation turns and replays them into the agent’s input as alternating user/assistant messages. There is no server-side thread state in the model.
  • Hands off to run_agent, then persists the final response on the way back out.

That’s the route → action layer. The interesting work starts inside run_agent.

Hop one: the agent decides what to call

The agent seeds its input list with system prompt, replayed turns, and the current query:

def _build_input_messages(ctx: AgentContext) -> list[dict[str, Any]]:
    messages = [{"role": "system", "content": build_knowledge_system_prompt()}]
    for turn in ctx.conversation_turns:
        messages.append({"role": "user", "content": turn.question})
        messages.append({"role": "assistant", "content": turn.answer})
    messages.append({"role": "user", "content": ctx.assistant_query_data.question.strip()})
    return messages

The model call uses OpenAI’s Responses API with three knobs that matter:

response = await client.responses.parse(
    model=AssistantConfig.OPENAI_ANSWER_MODEL,
    input=input_list,
    tools=tools,                         # JSON Schema for each registered tool
    tool_choice="auto",
    parallel_tool_calls=True,
    text_format=AssistantAnswerSchema,   # Pydantic-enforced output
)

tools= is the JSON-Schema list of registered tools the model can call. parallel_tool_calls=True lets the model request several tool calls in one response instead of one at a time. text_format=AssistantAnswerSchema constrains the final answer to a Pydantic schema, so once the model stops calling tools it cannot return free-form text.

For our ₹15 lakh query, hop 1’s output is three function calls in one response:

hop=1 model output:
  function_call: compute_income_tax
    arguments: { "gross_income": 1500000, "fy": "2025-26",
                 "regime": "both", "age_category": "general",
                 "deductions": { all six keys, all zero } }
  function_call: compute_income_percentile_forward
    arguments: { "income": 1500000 }
  function_call: retrieve_knowledge
    arguments: { "query": "marginal relief new regime 87A rebate threshold",
                 "decompose": false }

No final answer this hop. Just three calls. The loop body executes them and re-runs the model:

for hop in range(MAX_HOPS):
    response = await client.responses.parse(...)
    function_calls = _detect_function_calls(response)
    if not function_calls:
        return response.output_parsed   # final answer

    input_list.extend(_serialize_response_item(item) for item in response.output)
    for call in function_calls:
        output_str = await _execute_tool_call(call, ctx)
        input_list.append({
            "type": "function_call_output",
            "call_id": call.call_id,
            "output": output_str,
        })

MAX_HOPS = 3. Past the third hop, a forced final-pass message strips the tool registry and demands an answer. The loop cannot run away.

   seed messages: [system_prompt, ...history, current_query]
                                │
                                ▼
   ┌──────────[Responses API call with tools]────────────┐
   │                            │                         │
   ▼                            ▼                         │
function_calls?            final answer?                  │
   │                            │                         │
   │ yes:                       │ yes: return             │
   │  execute tools,            │                         │
   │  append outputs                                      │
   │                                                      │
   └──────── hop++ < MAX_HOPS=3 ──────────────────────────┘

The LLM is the dispatcher, not the calculator. A model that hallucinates tax math is a liability. A model that picks the right tool and forwards the user’s numbers is a feature.

The three tools fire

Each registered tool is a ToolSpec with four fields: a name, a description, a JSON Schema for its parameters, and an async handler that takes an AgentContext (a per-turn struct carrying request metadata, prior turns, and a collected_chunks field that retrieval tools deposit into) plus the model’s arguments and returns a dict. The model sees the first three; the handler runs server-side. For the query we’re tracing, the model exercises three tools: compute_income_tax, compute_income_percentile, and retrieve_knowledge. The first two are deterministic Python; the third is stochastic retrieval. That split, between tools whose output you can be confident in and tools whose output you cannot, is the architecturally interesting part of the registry, and we follow each in turn.

compute_income_tax

The tool wrapper at core/orchestrator/tools/compute_income_tax.py exists for one reason, and its description states it plainly:

ALWAYS call this tool for any tax-computation request. Do NOT compute slab tax yourself. … Copy the returned formatted_summary VERBATIM into your answer. Do not paraphrase numbers.

The calculator computes; the model transcribes.

Two layers sit behind that contract. The wrapper validates inputs and forwards to a pure-Python calculator at core/tools/calculators/income_tax.py: slab tables for FY 2025-26, 87A rebate, surcharge bands, 4% cess, two flavours of marginal relief (rebate-boundary and surcharge-threshold).

For our ₹15 lakh query, the model emitted:

INPUT  → compute_income_tax({
  "gross_income": 1500000,
  "fy": "2025-26",
  "age_category": "general",
  "regime": "both",
  "deductions": { "sec_80c": 0, "sec_80d": 0, "hra_exempt": 0,
                  "home_loan_interest": 0, "nps_80ccd_1b": 0,
                  "nps_80ccd_2_employer": 0 }
})

OUTPUT → {
  "new": { "taxable_income": 1425000, "base_tax": 93750,
           "cess": 3750, "total_tax": 97500,
           "effective_rate_pct": 6.5 },
  "old": { "total_tax": 257400 },
  "cheaper_regime": "new",
  "savings_under_cheaper": 159900,
  "formatted_summary": "Under new regime, tax on ₹15,00,000 is..."
}

Two things to notice. The model passed gross_income: 1500000 (a literal integer in rupees, not the string "15 LPA") because the tool description spells out the conversion. The tool returned 97500 after applying the default ₹75,000 standard deduction (1500000 - 75000 = 1425000 taxable). The model did neither computation. Both happened inside the calculator.

The calculator returns a typed IncomeTaxRegimeBreakdown:

class IncomeTaxRegimeBreakdown(BaseModel):
    regime: Literal["old", "new"]
    standard_deduction: int
    taxable_income: int
    base_tax: int
    rebate_87a: int
    surcharge: int
    cess: int
    marginal_relief_rebate: int
    marginal_relief_surcharge: int
    total_tax: int
    effective_rate_pct: float

The canonical case is pinned in a golden test:

def test_15L_new_regime_no_deductions():
    r = compute_income_tax(gross_income=1500000, regime="new")
    assert r.new.total_tax == 97500

Moving arithmetic out of the model is only useful if the calculator stays correct over time. These tests run in under a second and each pins a canonical case to a number.

compute_income_percentile

The percentile tool follows the same shape (a thin wrapper around a pure-Python function) and reads from a real ITR filing-statistics dataset. For our query, the model called it with the user’s income:

INPUT  → compute_income_percentile_forward({ "income": 1500000 })

OUTPUT → {
  "top_percent": 17.42,
  "percentile_from_bottom": 82.58,
  "financial_year": "2024-25",
  "formatted_summary": "₹15 lakh puts you in the top 17.42%..."
}

The 17.42% and 82.58% in the user-visible answer are these two fields, copied through.

retrieve_knowledge

The third tool that fired is the only one that isn’t a pure calculator. retrieve_knowledge wraps retrieve_relevant_chunks() in core/services/retrieval.py, which runs two retrievals in parallel and merges them:

query ──┬──▶ embed (text-embedding-3-small, 1536d) ──▶ pgvector ANN ─┐
        │                                                             ├─▶ RRF ─▶ top-K
        └──▶ plainto_tsquery('english', ...)  ─▶ tsvector / ts_rank ──┘
                                                          1 / (60 + rank)

Vector search nails semantic paraphrase (“set off” vs. “offset”). BM25 over the Postgres tsvector nails rare proper nouns the embedding flattens (“Section 115BBH”, “194A”). Reciprocal Rank Fusion lets both win without either dominating: each chunk scores Σ 1 / (60 + rank) across the two lists, top-K wins.

Chunking is heading-aware: every chunk has its breadcrumb prepended before embedding, so the heading context rides into both the embedding and the tsvector. For our marginal-relief leg, the chunk that ranked first came from the slabs document, retrieved with its breadcrumb intact:

Income Tax Slabs FY 2025-26 > Marginal Relief

Marginal relief applies at the 87A rebate boundary and at each surcharge
threshold. When income slightly exceeds a threshold, the additional tax
cannot exceed the additional income above the threshold.

The breadcrumb (Income Tax Slabs FY 2025-26 > Marginal Relief) is the difference between a paragraph that ranks first and one that sits unranked in embedding space. It is also what gave the model the “₹12 lakh 87A rebate threshold and surcharge thresholds” framing it used in its final answer.

Hop two: the agent narrates the answer

Tool outputs are JSON-serialized and appended to the message list as function_call_output items, each tagged with the call_id of the function call it answers. The loop re-runs client.responses.parse(...) against the enriched message list. This time, no tool calls. The model emits a final answer constrained to the AssistantAnswerSchema: an answer string, a reasoning_summary, a confidence value, a next_actions list, an optional disclaimer, and an internal topic field that the composer uses to dispatch.

For our query, hop 2’s parsed output:

hop=2 final output (parsed against AssistantAnswerSchema):
  confidence: "high"
  answer: "- In the new regime, tax on **₹15 lakh** is **₹97,500** for
           FY 2025-26.n- Under the old regime..."
  reasoning_summary: "- Used the income-tax calculator for FY 2025-26
           and the percentile tool for ₹15 lakh income..."
  next_actions: ["Compare old vs new for my deductions",
                 "What income is top 10%?",
                 "How is 87A rebate applied?"]
  disclaimer: null
  topic: <internal value that maps to the answered branch>

Every number in answer traces back to a tool output: 97500, 257400, 159900 from compute_income_tax; 17.42 and 82.58 from compute_income_percentile_forward; the “₹12 lakh 87A rebate threshold and surcharge thresholds” framing from a retrieved chunk. The model wrote the prose; it did not invent the figures. disclaimer: null is fine. The composer injects it based on which tools fired, not on what the model thinks. And if Pydantic parsing fails (e.g., next_actions has four items), the API errors and the loop catches it. The model never delivers an unstructured response downstream.

The composer ships it

The model’s AssistantAnswerSchema is internal. The public response (what the user’s browser ultimately receives) is AssistantResponseSchema: stricter rules, a coarser four-value status enum, and no topic field at all.

The translation happens in _compose_response:

def _compose_response(
    ctx: AgentContext,
    answer: AssistantAnswerSchema,
) -> AssistantResponseSchema:
    if answer.topic == "refused":
        return build_orchestrator_refused_response(...)
    if answer.topic == "out_of_scope":
        return build_orchestrator_out_of_scope_response(...)
    if answer.topic == "needs_clarification":
        return build_orchestrator_needs_clarification_response(...)
    return build_orchestrator_answered_response(
        request_id=ctx.request_id,
        conversation_id=ctx.conversation_id,
        answer=answer,
        retrieved_chunks=ctx.collected_chunks,
        tools_called=ctx.tools_called,
    )

Four branches, dispatched on topic. For refused and out_of_scope, the composer discards the model’s answer text and substitutes hardcoded copy. The model is welcome to refuse, but it is not welcome to write the refusal. Hardcoded text means legal review happens once, not every release. For needs_clarification, the model’s text passes through (the clarifying question must be context-specific) but citations and disclaimer are forced empty.

For our query, the topic falls through to the answered branch. The composer passes the model’s prose through, strips the internal topic field, looks at tools_called to inject the right citation (calculator answers get the income-tax-calculator CTA), and injects the FY-scoped CA disclaimer. The public schema then enforces hard constraints at the boundary: next_actions non-blank, max three. The model already agreed to this shape in the structured output; the composer re-checks at the seam anyway.

        topic enum (model output)
                  │
   ┌──────────────┼───────────────┬───────────────┐
   ▼              ▼               ▼               ▼
answered     needs_clarif.    out_of_scope     refused
   │              │               │               │
   │              │               ▼               ▼
   │              │       (discard model      (discard model
   │              │        text, hardcode      text, hardcode
   │              │        answer)             answer)
   │              │               │               │
   │              ▼               │               │
   │     (force citations=[],     │               │
   │      disclaimer=null)        │               │
   │              │               │               │
   ▼              ▼               ▼               ▼
        strip internal field (topic)
                       │
                       ▼
              AssistantResponseSchema  ──▶  public JSON

Structured output is a guardrail, not a convenience. If any model output can leak straight to users, you’ve outsourced your domain correctness to a stochastic process.

How we know it’s not lying

The whole post argues that the bot doesn’t compute its own numbers. That claim needs to be verified, not just asserted.

Two layers do the work.

Calculators are pinned with golden tests:

def test_5L_new_regime_full_rebate():
    r = compute_income_tax(gross_income=500000, regime="new")
    assert r.new.total_tax == 0
    assert r.new.taxable_income == 425000

def test_12L_new_regime_at_rebate_threshold():
    r = compute_income_tax(gross_income=1200000, regime="new")
    assert r.new.total_tax == 0

def test_marginal_relief_above_12L_new_regime():
    """Taxable just over 12L → total tax capped at the excess income."""
    ...

These run in under a second. Each names a canonical scenario and pins it to a number. The calculator is the source of truth for the assistant; the tests are the guard rail that keep the calculator honest as slabs and thresholds change year to year.

Retrieval is checked by a small golden-query eval. tests/retrieval/golden_queries.jsonl carries cases like:

{
  "id": "vda_loss_setoff_salary",
  "query": "I lost 50000 on Bitcoin, can I set off against my salary income?",
  "expected_text_substr": [
    "loss from transfer of virtual digital asset",
    "vda losses cannot be set off",
    "115bbh"
  ],
  "failure_mode": "TRUE_FAIL"
}

scripts/retrieval_eval.py runs each case through retrieve_relevant_chunks() and reports two numbers:

  • Hit@K: did any chunk in the top K contain at least one of the expected substrings (case-insensitive).
  • MRR: mean reciprocal rank of the first hit, averaged across cases.

Each case carries multiple substring variants; a chunk counts as a hit if any one matches. That trades precision for resilience: a single brittle needle would mean a rephrasing of the source paragraph fails the test for no real reason.

This set is intentionally small. It’s a smoke detector, not a quality measurement. But cheap evals catch regressions unit tests cannot. The unit tests verify code; the eval verifies behaviour. They run in CI and they have caught real shifts in chunk ranking that nothing else would have.

What this shape buys you

Three transferable ideas, stated plainly.

1. The model is a dispatcher, not the source of truth. Anything that has to be deterministic (numbers, business rules, contracts) belongs in a tool. The model picks tools and narrates results. Drawing the line between “this can be wrong” and “this must be right” is the boundary between the model and the calculators. A lot of decisions follow from that line.

2. Structured output is a guardrail. Pydantic-enforced output schemas turn the model’s free-text into a typed value the rest of your application can rely on. The composer is then where domain rules live, not the prompt. Tell the model what to do is brittle; make it impossible for the model to return the wrong shape is not.

3. A small golden eval is the cheapest production-confidence tool you have. A small set of retrieval goldens catches regressions a unit-test suite cannot see. A small set of calculator goldens catches what a retrieval eval cannot. Neither is a substitute for the other, and both are cheap enough to never not have.

The user who asked our anchor query got a correct answer not because the model is smart, but because the architecture refuses to let the model be wrong about anything that has a right answer. The calculator wrote the numbers. The retrieved chunk wrote the framing. The composer wrote the contract. The model wrote the prose. Each layer did one job, and only the prose layer was allowed to be creative.

Docker + Kubernetes 生产环境部署完全指南

Docker + Kubernetes 生产部署

Docker + Kubernetes 生产部署

在企业级应用开发中,容器化与编排技术已经成为基础设施的基石。本文将从实际生产场景出发,系统讲解 Docker 容器化到 Kubernetes 编排部署的完整工作流,帮助 DevOps 工程师和后端开发者构建可靠、高效的云原生应用交付体系。

一、Docker 容器化最佳实践

1.1 多阶段构建

多阶段构建是生产环境镜像优化的核心手段,能够将构建依赖与运行时环境严格分离,显著减小最终镜像体积。

# ===== 阶段一:构建阶段 =====
FROM node:20-alpine AS builder

WORKDIR /app

# 先复制依赖文件,利用 Docker 缓存层加速构建
COPY package.json package-lock.json ./
RUN npm ci --only=production

# 复制源码并构建
COPY . .
RUN npm run build

# ===== 阶段二:运行阶段 =====
FROM node:20-alpine AS runtime

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 仅从构建阶段复制必要产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

1.2 镜像优化要点

  • 选择精简基础镜像:优先使用 alpine、slim 等变体,避免安装不必要的系统包
  • 合并 RUN 指令:减少镜像层数,将相关操作用 && 连接
  • 利用构建缓存:将不常变化的文件(如依赖声明)放在 Dockerfile 前部
  • 设置 .dockerignore:排除 .git、node_modules、*.md 等无关文件
# .dockerignore
.git
.gitignore
node_modules
npm-debug.log
README.md
.env.local

二、Docker Compose 开发环境配置

在开发阶段,Docker Compose 能够快速搭建包含应用、数据库、缓存等完整依赖的本地环境。

# docker-compose.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DB_HOST=postgres
      - DB_PORT=5432
      - REDIS_HOST=redis
    volumes:
      - .:/app          # 挂载源码实现热重载
      - /app/node_modules
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpassword
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:
  redisdata:

三、Kubernetes 核心概念

在深入部署配置之前,先梳理 K8s 中最核心的几个资源对象:

资源对象 职责
Pod 最小调度单元,包含一个或多个共享网络与存储的容器
Deployment 声明式管理 Pod 副本数,支持滚动更新与回滚
Service 为 Pod 提供稳定的网络访问入口与负载均衡
ConfigMap 存储非敏感配置数据,以环境变量或卷挂载方式注入 Pod
Secret 存储敏感数据(密码、证书等),Base64 编码存储

四、生产级 Kubernetes 部署配置

以下是一个完整的生产级部署方案,包含 Deployment、Service、ConfigMap、Secret 和 Ingress。

4.1 Namespace 与 ConfigMap

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    env: production
    team: backend
---
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
data:
  NODE_ENV: "production"
  DB_HOST: "postgres-service"
  DB_PORT: "5432"
  DB_NAME: "myapp"
  REDIS_HOST: "redis-service"
  LOG_LEVEL: "info"

4.2 Secret

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: production
type: Opaque
data:
  # 值需经过 base64 编码
  DB_PASSWORD: c2VjcmV0cGFzc3dvcmQ=
  JWT_SECRET: eW91ci1qd3Qtc2VjcmV0LWtleS0yMDI2
  API_KEY: YWJjZGVmZzEyMzQ1Njc4

注意:生产环境中建议使用 Sealed Secrets、HashiCorp Vault 或云厂商的密钥管理服务来管理敏感数据,避免将 Secret 明文提交到代码仓库。

4.3 Deployment

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
  labels:
    app: myapp
    version: v1.2.0
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # 滚动更新时最多多出 1 个 Pod
      maxUnavailable: 0    # 更新期间不允许有 Pod 不可用
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
        version: v1.2.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "3000"
        prometheus.io/path: "/metrics"
    spec:
      serviceAccountName: myapp-sa
      terminationGracePeriodSeconds: 60

      # 反亲和性:确保 Pod 分散在不同节点
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - myapp
                topologyKey: kubernetes.io/hostname

      containers:
        - name: myapp
          image: registry.example.com/myapp:v1.2.0
          ports:
            - containerPort: 3000
              protocol: TCP

          # 资源限制与请求
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

          # 环境变量注入
          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: app-secrets

          # 健康检查
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 20
            timeoutSeconds: 5
            failureThreshold: 3

          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3

          startupProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 0
            periodSeconds: 5
            failureThreshold: 12   # 最多等待 60 秒启动

          # 生命周期钩子
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 15"]

          # 安全上下文
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false

4.4 Service 与 Ingress

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: myapp
  ports:
    - name: http
      port: 80
      targetPort: 3000
      protocol: TCP
---
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls-secret
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp-service
                port:
                  number: 80

五、健康检查与自动扩缩容

5.1 健康检查设计

K8s 提供三种探针(Probe),分别应对不同场景:

  • livenessProbe:检测容器是否存活,失败则重启容器
  • readinessProbe:检测容器是否就绪,失败则从 Service Endpoints 中摘除
  • startupProbe:检测容器是否启动完成,适用于启动较慢的应用

5.2 HPA 自动扩缩容

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 3
  maxReplicas: 15
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60

HPA 的 behavior 配置至关重要:扩容可以激进一些以快速响应流量增长,缩容则应保守以避免频繁波动。

六、滚动更新与回滚策略

6.1 滚动更新

K8s Deployment 默认采用滚动更新策略。在上述 Deployment 配置中,我们设置了 maxSurge: 1 和 maxUnavailable: 0,这意味着更新时会先创建新 Pod,确认其就绪后再终止旧 Pod,实现零停机部署。

# 执行滚动更新
kubectl set image deployment/myapp 
  myapp=registry.example.com/myapp:v1.3.0 
  -n production

# 查看更新状态
kubectl rollout status deployment/myapp -n production

# 查看更新历史
kubectl rollout history deployment/myapp -n production

6.2 回滚

当新版本出现问题时,可以快速回滚到上一个稳定版本:

# 回滚到上一版本
kubectl rollout undo deployment/myapp -n production

# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=2 -n production

# 查看当前版本
kubectl rollout history deployment/myapp -n production

七、监控与日志

7.1 Prometheus + Grafana 监控栈

在 Deployment 中已通过注解暴露了 /metrics 端点。以下是 Prometheus 的 ServiceMonitor 配置:

# servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: myapp-monitor
  namespace: production
  labels:
    release: prometheus
spec:
  selector:
    matchLabels:
      app: myapp
  endpoints:
    - port: http
      path: /metrics
      interval: 15s
      scrapeTimeout: 10s

7.2 日志收集

生产环境推荐使用 EFK(Elasticsearch + Fluentd + Kibana)或 PLG(Promtail + Loki + Grafana)日志栈。以下是一个 Fluentd DaemonSet 的核心配置片段:


yaml
# fluentd-configmap.yaml(节选)
apiVersion: v1
kind: ConfigMap
metadata:

---

> 📢 **本文为精简版,完整版包含独家工具推荐和深度分析,请访问 [WD Tech Blog](https://wdsega.github.io) 查看!**

*关注我的博客获取最新科技资讯、AI教程和效率工具推荐!*

Running on No Sleep: Learning Progress Pt.12

Daily learning part twelve

Today I started at 16:00. I didn’t do any JavaScript sections because I’m at the end of the course, so I’m doing small projects to make me stronger at scripting.

I started by giving the book I bought yesterday a look and testing different MAC spoofing tools. I plan to make a tool myself, but not right now – maybe on the weekend.

IP LOOKUP SCRIPT
From 16:30 to 18:00 I was scripting a really simple IP lookup script in Python. It might be simple for some of you, but for me as a beginner it took me an hour and a half. Most of that time was spent staring at the screen and trying to figure out why my code wasn’t working.

I tried debugging it, but I set the breakpoint on the function call. When I stepped over it, the debugger just ran the function and moved to the next line without showing me anything. I thought my code was broken, but I had simply forgotten to put print() around the function call.

Vulnerability Class and Severity Filter
The second script I made was a Vulnerability Class and Severity Filter. This one took me about an hour. It took way longer to understand the concept than to actually script it. It’s a really simple script – I think I just wasn’t fully concentrated during this one. Anyway, here is the script:

Vulnerability Reporter in JavaScript
This was the last one and it took me an hour and a half. It was easy to understand, but the lack of sleep does its thing. Here is the code:

The last three hours were spent dealing with grep, trying to understand query parameters, watching videos on web vulnerabilities, and enrolling in the OWASP Top 10 IAAA Failures room on TryHackMe. In web parameter tampering I learned how to spot vulnerabilities like this:

<input type="hidden" id="1008" name="cost" value="70.00">

I need to keep these skills sharp — any recommendations?
Anyway, the last thing I did was build my next two‑week roadmap.
I finished the session at 23:40.

For now, I’m sticking to the plan. Tomorrow I’ll start with curl exercises , then move on to the Access Control lab. I’ll report back on how it goes.

AI Workflow Optimization: Snowflake Cortex, Claude Code Performance & Productivity

AI Workflow Optimization: Snowflake Cortex, Claude Code Performance & Productivity

Today’s Highlights

Today’s highlights cover critical performance insights for Snowflake Cortex Code, practical strategies for optimizing Claude Code workflows, and an experienced developer’s perspective on integrating AI for daily backend development.

Why Snowflake Cortex Code has sub-par performance? (r/dataengineering)

Source: https://reddit.com/r/dataengineering/comments/1tgqfhu/why_snowflake_cortex_code_has_subpar_performance/

The article uncovers a critical performance bottleneck within Snowflake Cortex Code, revealing a substantial ~25,000 token system prompt overhead at the start of each session. This significant pre-loaded prompt, which includes extensive skill descriptions for tools often irrelevant to a user’s specific task, consumes a large portion of the model’s available context window before any user input is processed. This design choice not only limits the effective processing capacity for actual queries but also leads to increased token costs and slower response times.

For developers leveraging Snowflake for AI-driven applications, this insight is vital. It underscores the importance of understanding the underlying architecture of managed AI services and its direct impact on efficiency, scalability, and operational costs in production environments. Addressing such hidden overheads is paramount for optimizing applied AI workflows, especially when dealing with large datasets or complex, multi-turn interactions where every token and millisecond counts.

Comment: This explains why some AI platforms feel sluggish or expensive. Developers need to be aware of hidden prompt overheads in managed AI services, as it directly impacts model effectiveness and operational costs in real-world applications.

11 Claude things I wish someone had told me 12 months ago (r/ClaudeAI)

Source: https://reddit.com/r/ClaudeAI/comments/1tgqnsl/11_claude_things_i_wish_someone_had_told_me_12/

This post promises to deliver practical, non-superficial tips for maximizing the utility of Claude, specifically in the context of “Claude Code” and its “Projects feature.” Unlike generic advice, the author, an 18-month daily Claude user, highlights specific strategies that profoundly changed their workflow, including six months dedicated to Claude Code. For developers, effectively leveraging features like “Projects” within an AI assistant is crucial for managing complex coding tasks, maintaining context across sessions, and integrating the AI into a structured, efficient workflow.

Such insights likely encompass advanced prompt engineering techniques, smart context management to overcome token limits, and organizational strategies that transform Claude from a simple chatbot into a powerful development co-pilot. This content directly addresses the application of AI tools to real development workflows, aiming to enhance productivity and streamline the code generation and debugging process.

Comment: Leveraging features like “Projects” in Claude Code is a game-changer for long-running code generation tasks, helping maintain context and structure for complex development workflows. Essential reading for better Claude integration.

Backend dev for 11 years. Honest question about my Claude Code days (r/ClaudeAI)

Source: https://reddit.com/r/ClaudeAI/comments/1tghcyc/backend_dev_for_11_years_honest_question_about_my/

An experienced backend developer with 11 years in the field shares a candid reflection on their extensive use of Claude Code, having moved the majority of their work into the AI tool over the past eight months. This post offers a valuable, first-hand account of integrating a powerful AI code generation tool into a professional, daily workflow. The “honest question” posed suggests a critical examination of the practicalities, benefits, or perhaps unforeseen challenges of such deep integration, moving beyond superficial usage.

For readers keenly interested in “applied AI,” “workflow automation,” and “production deployment patterns”—particularly in the realm of developer tools—this provides unique insight into how AI can profoundly alter a seasoned professional’s routine. It explores the evolving dynamics of human-AI collaboration in software development, offering a crucial perspective on the efficacy and future implications of relying on AI for core engineering tasks.

Comment: This perspective from an experienced backend dev is key for understanding AI’s practical integration into engineering workflows. It offers insights into how Claude Code alters daily coding routines and its long-term implications.

Building a Rental App in a Market Dominated by One Player

When Daft.ie has this much market share in a vertical, the product question is less “how do we beat them” and more “where are the gaps they can’t or won’t fill.” This is what I learned building into that market.

The incumbent’s structural advantages

Daft has a network effect problem for any competitor: landlords list where renters search, renters search where landlords list. Daft has had 20+ years to build that flywheel. The listing density is the moat, not the product.

Trying to compete on listing volume as a new entrant is a losing game. You can’t persuade landlords to post on your platform when you have no renters, and you can’t attract renters when you have no listings. This is the classic marketplace cold-start problem with an unusually entrenched incumbent.

The product decision I made early: don’t try to be a competing listing marketplace. Be a better search and discovery layer on top of the existing market. That means aggregating sources rather than becoming one.

What aggregation unlocks

If you’re the aggregation layer, you don’t need landlords to list with you. You pull from existing sources, including Daft, and show renters a more complete picture. The value proposition to renters doesn’t depend on convincing landlords to change behavior.

The technical challenge is that no portal exposes a public API for listing data. Everything is scraped. That means maintaining extractors for each source, handling anti-scraping measures, normalizing inconsistent data formats, and keeping everything fresh. It’s more engineering than a single portal requires, but it removes the cold-start problem entirely.

It also means the incumbent is complementary rather than an adversary. Daft has listings. I surface those listings with better search tools alongside listings from sources Daft doesn’t aggregate. I’m adding value on top of Daft’s data, not competing for their listing volume.

The product layer on top of data

Raw aggregation is a commodity play. It’s useful but easy to copy. The defensible layer is what you build on top of the data.

For HomeScout the differentiating features are:

Natural language search. Daft has dropdown filters. If you want “2-bed near the DART, under 1800, pet-friendly” you have to translate that into Daft’s filter vocabulary yourself, and some of it (transport proximity, pet policy across all listings) isn’t filterable at all. Parsing intent from freetext and executing it as a structured query is a real improvement for search quality.

Cross-source comparison. When listings are normalized to the same format, side-by-side comparison is possible across sources. On Daft you can only compare within Daft.

Alerts with full-market coverage. Setting up alerts on Daft gives you Daft’s inventory. Setting up alerts that cover 90+ sources gives you the full visible market. That coverage difference matters in a fast-moving market.

Application tracking and AI tools. Once someone finds a listing and wants to apply, the process is still manual. AI-assisted lease review and email drafting are add-ons that help at the next stage of the funnel.

What I got wrong early on

My first instinct was to compete on data volume: as many listings as possible, fresh as possible. That led me to over-invest in scraping infrastructure before I understood what users actually wanted.

Users didn’t want more listings. They wanted less noise and faster paths to listings that actually matched their criteria. The search and alerting quality mattered more than marginal improvements in source coverage beyond a certain threshold.

I also underestimated how much of the market moves through channels I can’t reach: word of mouth, agency email lists, existing tenant networks. No aggregator touches those. Setting user expectations appropriately matters.

The honest competitive picture

Daft will always have advantages I don’t: direct landlord relationships, a recognized brand, the listing density flywheel. Competing directly on those dimensions doesn’t make sense.

The space for a different product is on the search and tooling side for renters. Better search, better alerts, better comparison, better tools for the application process. None of those require being the listing source. They require being useful on top of the listing sources that exist.

That’s a narrower position than being a full marketplace, but it’s defensible and it’s genuinely useful in a way that the existing tools aren’t.

I wrote a user-facing comparison of the apps in this market at https://homescout.io/guide/best-rental-apps-ireland with less of the founder perspective and more of the practical breakdown.

Caspar Bannink. Founder of HomeScout.io. Building AI-powered rental search for Dublin.

DuckDB EC2 Optimization, Postgres FDW Pushdown, SQLite NetBeans Connectivity

DuckDB EC2 Optimization, Postgres FDW Pushdown, SQLite NetBeans Connectivity

Today’s Highlights

This week features crucial insights into optimizing DuckDB performance on cloud instances, a deep dive into PostgreSQL FDW pushdown strategies, and practical guidance on connecting SQLite databases to development environments. These stories offer actionable advice for enhancing database efficiency and integration.

Optimising DuckDB performance on large EC2 instances (r/dataengineering)

Source: https://reddit.com/r/dataengineering/comments/1tgpr0d/optimising_duckdb_performance_on_large_ec2/

This post delves into strategies for maximizing DuckDB’s performance when deployed on large AWS EC2 instances. DuckDB, known for its in-process analytical capabilities, can greatly benefit from proper configuration in high-resource cloud environments. The discussion likely covers critical aspects such as memory allocation, CPU core utilization, I/O optimization for data storage (e.g., S3, local NVMe), and concurrent query execution settings. Understanding these parameters is key to leveraging DuckDB effectively for intensive analytical workloads.

Effective optimization of DuckDB on cloud infrastructure is crucial for data professionals handling large datasets and complex analytical queries. This includes understanding how to leverage instance-specific hardware features, fine-tuning DuckDB’s internal parameters, and potentially adapting data loading or querying patterns to the cloud environment. Such insights can lead to significant improvements in query response times and overall operational efficiency for data pipelines.

Readers can expect practical tips and potentially code snippets or configuration examples for setting up DuckDB in a performant manner on AWS EC2. This topic is vital for those pushing DuckDB into production or scaling their data analysis pipelines using this powerful embedded analytical database, ensuring they extract maximum value from their cloud resources.

Comment: Understanding how to correctly configure DuckDB for specific cloud environments like EC2 is essential for getting the most out of its impressive analytical capabilities; this guide could be a performance game-changer for cloud-native data engineers.

Postgres FDW: Pushdown is a negotiation (r/PostgreSQL)

Source: https://reddit.com/r/PostgreSQL/comments/1tg4cc0/postgres_fdw_pushdown_is_a_negotiation/

This piece explores the intricacies of Foreign Data Wrappers (FDW) in PostgreSQL, specifically focusing on the concept of “pushdown.” FDWs allow PostgreSQL to query data residing in external data sources (like other PostgreSQL instances, MySQL, or even CSV files) as if they were local tables. The “pushdown” mechanism is a critical optimization where query operations (such as filtering, joining, or aggregation) are pushed down to the remote server for execution, significantly reducing the amount of data transferred over the network and processed locally.

The title “Pushdown is a negotiation” implies a nuanced discussion around how PostgreSQL’s query planner interacts with foreign servers to determine which operations can be efficiently delegated. This involves understanding the capabilities of the remote data source, the FDW’s implementation, and the query’s complexity. A robust FDW implementation can intelligently negotiate with the foreign server to offload as much work as possible, while a less capable one might result in fetching too much raw data, leading to inefficient data transfer and slow queries.

This topic is highly relevant for architects and developers building data pipelines or integrating disparate data systems using PostgreSQL. It offers valuable insights into performance tuning for distributed queries, helping users design more efficient FDW setups and write queries that maximize the benefits of remote processing by leveraging the foreign server’s capabilities where appropriate.

Comment: Mastering FDW pushdown is key for efficient cross-database querying in PostgreSQL, turning a potential bottleneck into a powerful distributed execution strategy for complex data integrations.

How can I connect a SQLite Database to NetBeans? (r/database)

Source: https://reddit.com/r/Database/comments/1tg1miq/how_can_i_connect_a_sqlite_database_to_netbeans/

This community post addresses a common, practical challenge for developers: connecting a SQLite database to an Integrated Development Environment (IDE) like NetBeans, especially for users on Linux Mint. SQLite, being an embedded, file-based database, is frequently used in desktop and mobile applications, making seamless integration with development environments crucial. The post highlights the difficulty in finding up-to-date, platform-specific guides, indicating a widespread need for clearer documentation or community-contributed solutions that address OS and IDE version variances.

Connecting SQLite to NetBeans typically involves adding the appropriate JDBC driver (e.g., sqlite-jdbc) to the project’s classpath, then using standard Java Database Connectivity (JDBC) APIs to establish a connection, execute SQL queries, and manage data. The challenge often lies in correctly configuring the driver, handling file paths (especially with OS-specific conventions), and ensuring compatibility across different operating systems and NetBeans versions. Debugging connection issues can be time-consuming without clear guidance.

For developers working with embedded databases, understanding these connection patterns is fundamental to building and testing their applications efficiently. A good answer to this query would provide a step-by-step guide, including dependency management (e.g., Maven/Gradle for the JDBC driver), concrete code examples for connection pooling or basic CRUD operations, and troubleshooting tips specific to Linux environments, enabling developers to integrate SQLite confidently into their Java projects.

Comment: This practical query underscores the need for clear, up-to-date, and platform-specific guides on integrating SQLite with common IDEs like NetBeans, which is a foundational skill for embedded database development.

Building a Rent Fairness Calculator From 10,000+ Listing Data Points

Dublin has a rental price opacity problem. Property sale prices in Ireland are public, recorded in the Property Price Register with address and amount for every transaction. Rental prices are not. The official rental registry (RTB) runs 3-6 months behind, covers only registered tenancies, and publishes aggregate figures rather than per-property data. The salary you’d need to comfortably rent in Dublin (we broke that down here) makes this opacity particularly painful.

If you’re a renter in Dublin trying to answer “is what I’m paying fair,” you’re working from a dataset of size one: the price you were offered, compared to a handful of other listings you happened to see when you were searching.

The product feature I wanted to build was a rent fairness calculator: given a property’s attributes, where does its price sit relative to the current market? That meant building the data layer first. Here’s what that involved.

The data source

Rental listing prices are the closest thing to real-time rental price data that exists in Ireland. They’re asking prices, not agreed prices. In a market with moderate negotiation (Irish rentals don’t negotiate much; properties tend to let at asking or the landlord adjusts and re-lists), asking prices are a reasonable proxy.

I aggregate listings from 90+ sources: the main portals (Daft, Rent.ie, MyHome), letting agency sites, property management company portals. Each listing has a price, location, size, bedroom count, and various attributes.

The working dataset for price analysis covers Dublin listings over a rolling window. At any given time, the active dataset is several thousand current listings. The historical set across all sources runs into the tens of thousands of records.

The normalization problem

Raw listing data from different sources is not comparable without normalization. The specific issues:

Price format. Some sources post weekly prices, some monthly. Some include bills, some don’t. Agency sites sometimes post “pcm” (per calendar month) in some listings and “pw” (per week) in others. All prices get converted to monthly EUR at ingestion. This sounds simple and has about thirty edge cases.

Bedroom count. “2 bedrooms + box room” is listed as 2 bedrooms on some sites and 3 on others. A box room is not a standard bedroom but some landlords list it as one to justify a higher price. I normalize to functional bedrooms, which means the box room goes to a separate field. This affects price comparisons: if you’re comparing a 2-bed to other 2-beds and one of them has a box room that another landlord would call a third bedroom, your benchmark is off.

Location. This is the hardest one. “Rathmines,” “Dublin 6,” “D6,” and “Rathmines Village” are all the same thing depending on which source you’re reading. Address normalization across sources requires fuzzy matching, postal district parsing, and a lookup table that maps informal neighborhood names to canonical areas.

I use a two-stage location resolution: first parse to lat/lng from the address string (using a geocoding API for addresses that parse cleanly, fuzzy matching for ones that don’t), then map lat/lng to neighborhood polygon using a Dublin neighborhood boundary dataset. The neighborhood polygon layer is what’s used for price comparisons.

BER rating. Every listing in Ireland is legally required to have a BER (Building Energy Rating). In practice, some listings omit it. For listings with a rating, it’s a structured value (A1 through G) and reliable. For listings without, the field is null and treated as unknown.

The comparison model

With normalized data, the price fairness question becomes: given this property’s bedroom count, neighborhood, and BER rating, what’s the price distribution for comparable properties right now?

The comparison model groups listings by:

  • Bedroom count (exact match)
  • Neighborhood (same neighborhood polygon, or adjacent neighborhoods if the sample in the exact neighborhood is too small)
  • BER band (A/B, C, D, E/F/G) to avoid comparing an A-rated property to a G-rated one

Within that group, I calculate the median, 25th percentile, and 75th percentile of monthly price. The output for any given listing is where that listing’s price falls in the distribution.

The framing in the product is deliberate: “this property is in the top quartile for 2-beds in Rathmines at BER C” rather than “this property is overpriced.” Whether the premium is justified is a user judgment, not a data judgment. Maybe it has better light. Maybe the landlord is unusually responsive. The data removes the information gap; it doesn’t replace the decision.

The sample size problem

Dublin’s neighborhoods vary significantly in listing volume. D2 (city centre) and D6 (Rathmines/Ranelagh) have enough active listings at any time that a 2-bed price distribution is meaningful. Smaller neighborhoods might have 3-4 active 2-beds at any point.

When the sample in the target neighborhood is too small (below a threshold I set at around 8-10 listings), I expand to adjacent neighborhoods. This introduces some noise because adjacent neighborhoods aren’t identical markets. I flag to the user when the comparison is using an expanded geographic area so they understand the basis.

The alternative was to only show price context when the sample is large enough to be reliable. That turns out to leave a lot of users without any context because most Dublin suburbs have thin listing volume at any given bedroom count. The expanded geography approach, with transparency about what’s being expanded, is more useful in practice.

Historical trends

With enough historical data, you can calculate trends: how has the median price for a 2-bed in Rathmines changed over the past 12 months? This is useful for users trying to time their search or understand whether the market is moving.

The limitation: we’ve been running long enough to have meaningful trend data, but not long enough to have multi-year trends. The next 12-18 months of data will make this feature significantly more useful.

The thing I can’t build from listing data: agreed prices. If a landlord lists at EUR 2,200 and lets at EUR 2,050, the agreed price data never reaches me. The gap between asking and agree in Dublin is probably 2-5% in most areas (the market doesn’t negotiate much), but “probably” isn’t good enough if you want precise fairness benchmarks. The RTB data eventually captures this but with a lag that makes it useless for current decisions.

Implementation notes

The pipeline: Python for crawling and parsing, Postgres for storage, dbt for the normalization and comparison model transformations. The price distribution calculations are pre-computed on a daily schedule rather than run at query time, which keeps the user-facing API fast.

The geocoding step is the slowest part of ingestion, roughly 200-400ms per address with a good geocoding provider. Cached aggressively; addresses that have already been geocoded don’t hit the API again.

The user-facing version — how to actually use this data to check your own situation — is at https://homescout.io/guide/am-i-overpaying-rent-dublin-2026. Less pipeline detail, more “here’s the number and what it means.”

Caspar Bannink. Founder of HomeScout.io. Building AI-powered rental search for Dublin.

How Natural Language Search Actually Works for Property Listings

Most NLP tutorials use toy examples. “What’s the weather?” “Book me a flight.” Property search is structurally messier than either, and the failure modes are worth understanding before you build anything.

Here’s how I implemented natural language search for Dublin rental listings, what broke, and what actually works.

The problem with filter dropdowns

Classic rental portals give you: min price, max price, min beds, max beds, area (dropdown), and maybe a keyword field. That’s fine if you know exactly what you want and the options the site gives you match how you think. Most tools for finding apartments in Dublin are built around this exact model.

In practice, people search like this:

“2-bed near the DART, pet-friendly, under 1800, ground floor preferred”

That query has five separate intent signals. A dropdown UI forces you to split those across multiple controls, remember which areas are “near the DART”, manually scan results for pet-friendly mentions, and completely ignore “ground floor” because no portal has that filter.

The fix isn’t just “add more filters.” It’s parsing intent directly from text.

Intent extraction pipeline

My pipeline has four stages:

1. Query parsing with a structured extraction prompt

I use GPT-4o with a system prompt that instructs it to return a JSON schema. The schema has typed fields: max_price, min_beds, max_beds, transport_proximity (array of station names), pet_friendly (bool or null), floor_preference, areas (array), keywords (freetext remainder).

The LLM handles the conversion from natural language to typed values. “Under 1800” becomes max_price: 1800. “Near the DART” becomes an intent flag that hits a second lookup stage.

2. Geographic intent resolution

“Near the DART” is ambiguous. The DART runs from Malahide to Greystones. You have to ask: how near? Which stations matter for this user?

I don’t try to resolve this in the LLM prompt. Instead I maintain a static lookup table mapping Dublin neighborhoods and transport stops to lat/lng bounding boxes. “Near the DART” expands to a union of bounding boxes within ~1km of each station. If the user said “near Sandymount DART” that narrows it to one station’s box.

This is faster, cheaper, and more predictable than asking the LLM to do geography.

3. Structured query execution

Now I have a typed query object. This goes against the listings database as a standard structured query. Postgres, indexed on price, beds, area polygons. No vector search at this stage. Structured data matches structured queries better than embeddings do.

4. Semantic re-ranking

For the freetext remainder that doesn’t map to structured fields (“quiet street”, “modern kitchen”, “nice landlord”), I use embeddings. The listing description text is embedded at index time (OpenAI text-embedding-3-small). The freetext query fragment is embedded at search time. Cosine similarity gives a re-ranking score that gets blended with the structured match score.

The blend ratio matters. Too much weight on embeddings and structured matches get buried. Too little and you lose the semantic layer entirely. I landed on roughly 70% structured, 30% semantic after testing on actual Dublin listing data.

What GPT gets wrong on real estate data

A few failure modes I hit:

Price hallucination. Early prompts without strong grounding would occasionally invent constraints. “Around 1800” might become max_price: 1750 or max_price: 1900 depending on prompt phrasing. Fix: require exact numeric extraction and default to null when the query is ambiguous, then ask the user to clarify.

Overfitting to query phrasing. If someone writes “not too far from town” the LLM might return areas: ["Dublin 1", "Dublin 2"] with high confidence. That’s one interpretation. Another user might mean the same thing but expect Rathmines to be included. The structured query I build from that is deterministic; the interpretation step isn’t.

Missing negation. “No agency fees” or “not a studio” are common. LLMs handle negation inconsistently in structured extraction. Explicit prompt engineering helps but doesn’t fully solve it.

Listings data quality. Even with perfect parsing, listings have inconsistent data. “Pet-friendly” might be mentioned in the description, in an amenity tag, or not at all even if it’s true. The semantic layer helps here, but it’s not a complete solution.

Embeddings for listings: when they help, when they don’t

Embeddings are good at capturing soft preferences that don’t map to structured fields. “Quiet” vs “vibrant.” “Modern” vs “period features.” These are real preferences that affect decisions.

They’re bad at exact constraints. If someone says “max 1800” and a listing is 2100, cosine similarity between “affordable” and a description mentioning “prime location” might still score it highly. Structured filtering has to happen first, before embeddings get anywhere near the results.

The architecture I landed on: structured query first (hard filters), then embedding re-rank within the filtered set. Never embedding search across the full corpus for price-sensitive queries.

The index structure

Each listing in the database has:

  • Structured fields: price, beds, baths, area, lat/lng, available date, pet policy (when known), parking
  • A generated text blob: all descriptive text from the listing concatenated
  • A precomputed embedding of that text blob
  • Source metadata for deduplication across the 90+ sites we aggregate

At search time: structured WHERE clause narrows the set, then embedding similarity re-ranks within it. Results return in under 200ms for most queries on the current Dublin dataset size.

What I’d do differently

If I were starting over, I’d invest earlier in the geographic resolution layer. Dublin’s transport network is the single biggest intent signal in rental searches here, and the freetext-to-station-to-bbox mapping took longer to get right than the LLM integration.

I’d also be more conservative about what the LLM touches. The more you can push to deterministic lookups and structured queries, the more predictable the system is. LLMs are best at the translation step, not the search step.

The non-technical side of all this — why the search actually feels different for users — is covered in the user-facing guide on the HomeScout blog.

Caspar Bannink. Founder of HomeScout.io. Building AI-powered rental search for Dublin.

5 AI Prompts That Get You Production-Ready Code Every Time (Copy-Paste Ready)

If you’ve ever typed “write me a REST API” into
ChatGPT and got back something you had to completely
rewrite — this post is for you.

The problem isn’t the AI. It’s the prompt.

After testing 200+ prompts across Claude, ChatGPT,
and Gemini, I found one framework that consistently
produces production-ready code: CRTSE.

What is CRTSE?

Every great AI prompt has 5 parts:

  • C — Context: What project/situation you’re in
  • R — Role: What expert the AI should act as
  • T — Task: Exactly what you want done
  • S — Standards: Quality constraints and requirements
  • E — Example: A complete ready-to-use prompt

Here are 5 prompts built with this framework
that I use every week:

Prompt 1 — REST API Endpoint Generator

The problem it solves: You need a complete,
production-ready API endpoint but don’t want to
write all the boilerplate from scratch.

Copy this prompt:

You are a senior Node.js backend engineer with
10+ years of experience. Create a complete POST
/api/users endpoint in TypeScript using Express +
Zod validation + Prisma ORM + JWT auth middleware.
Return full code for routes/users.ts and
middleware/auth.ts. Handle duplicate email (409),
invalid input (400), and server errors (500).
Include JSDoc comments. No external dependencies
beyond Express, Zod, Prisma, and jsonwebtoken.

Why it works: The role assignment (“senior
Node.js backend engineer”) activates the model’s
most relevant knowledge. The standards section
(“no external dependencies”) prevents bloated
output. The error handling specification forces
production-level thinking.

Result: Full working code in one shot. No
rewriting needed.

Prompt 2 — Memory Leak Detector

The problem it solves: Your Node.js service
memory grows over time and you can’t find why.

Copy this prompt:

You are a Node.js memory profiling expert
specializing in V8 heap analysis and garbage
collection. Review this code: [paste your code].
Identify: 1) All memory leak sources with exact
line references, 2) Why V8 GC cannot collect
them, 3) Fixed version of each leaky function
with before/after comparison, 4) Exact commands
to profile with –inspect and take heap snapshots,
5) How to set up memory monitoring alerts in
production.

Why it works: Asking for “exact line references”
forces precision. Asking for “before/after
comparison” ensures you get actionable fixes,
not just analysis.

Result: I used this last week and found 3
event listener leaks in 2 minutes that I had
been chasing for hours.

Prompt 3 — SQL Query Performance Debugger

The problem it solves: Your database queries
are running slowly in production and you don’t
know why.

Copy this prompt:

You are a PostgreSQL performance engineer.
Here is my slow query and EXPLAIN ANALYZE
output: [paste query + EXPLAIN output].
Provide: 1) Identified bottlenecks (seq scans,
missing indexes, bad joins), 2) CREATE INDEX
statements to add, 3) Optimized query rewrite,
4) Expected performance improvement percentage,
5) Any schema changes recommended. Target
sub-100ms execution time.

Why it works: Giving the AI the EXPLAIN
ANALYZE output is the key. It stops the AI
from guessing and forces it to analyze your
actual query plan.

Result: Cut a 4-second query down to 60ms
using the indexes this prompt recommended.

Prompt 4 — Security Vulnerability Auditor

The problem it solves: You want a security
review of your backend before pushing to
production.

Copy this prompt:

You are an application security engineer with
OWASP expertise. Security audit this code:
[paste code]. For each vulnerability: 1) OWASP
Top 10 category, 2) Severity rating
(Critical/High/Medium/Low), 3) Exact vulnerable
line with explanation, 4) Fixed code snippet.
End with a security hardening checklist.
Priority order: SQL injection, auth bypass,
sensitive data exposure, broken access control.

Why it works: The OWASP category requirement
forces structured output. The severity rating
tells you what to fix first. The priority order
at the end means critical issues always surface
first.

Result: Found a JWT verification bypass in
my code that would have been a critical
production vulnerability.

Prompt 5 — Code Refactoring Planner

The problem it solves: You inherited legacy
code and need a clear refactoring plan without
breaking everything.

Copy this prompt:

You are a software architect who applies SOLID
principles and clean code practices. Analyze
this code for code smells: [paste code]. For
each smell: 1) Formal name (e.g. Long Method,
God Class), 2) SOLID principle violated,
3) Refactored version using the appropriate
design pattern, 4) Risk level of refactoring
(High/Medium/Low). Prioritize smells that will
cause the most maintenance pain in 6 months.

Why it works: Asking for “formal names”
gives you vocabulary to discuss with your team.
The “6 months” framing forces the AI to think
about long-term maintainability, not just
cosmetic fixes.

Result: Turned a 400-line God Class into
4 focused services with clear responsibilities.

The Pattern Behind All 5 Prompts

Look at what every prompt above has in common:

✅ Specific expert role — not just “you are
an expert” but “you are a PostgreSQL performance
engineer”

✅ Numbered output format — forces structured,
scannable responses

✅ Quality constraints — sub-100ms target,
OWASP categories, severity ratings

✅ Edge case handling — duplicate emails,
server errors, null inputs

✅ Actionable deliverable — not analysis,
but working code or specific commands

This is the CRTSE framework and it works across
Claude, ChatGPT, Gemini, and GitHub Copilot.

Want All 50?

I packaged 50 of these prompts into a PDF
covering 5 categories:

→ Code Generation (10 prompts)
→ Debugging (10 prompts)
→ Code Review (10 prompts)
→ Documentation (10 prompts)
→ Refactoring (10 prompts)

Each prompt follows the full CRTSE framework
with a complete copy-paste ready example.

Get the full pack here → Ai Prompts for Developers

What’s the best AI prompt you’ve used for
coding? Drop it in the comments — I read
every one. 👇

GPU Bottleneck Analyzer, NVIDIA Rubin VRAM Demands, and Qwen VRAM Optimization

GPU Bottleneck Analyzer, NVIDIA Rubin VRAM Demands, and Qwen VRAM Optimization

Today’s Highlights

This week’s top GPU news features a new open-source tool for identifying PyTorch/CUDA bottlenecks, critical insights into NVIDIA’s future Rubin AI platform’s immense VRAM requirements, and a practical guide to optimizing Qwen 3.6 27B’s VRAM usage on 24GB GPUs.

Built an open source GPU bottleneck analyzer for PyTorch/CUDA. Looking for honest feedback (r/CUDA)

Source: https://reddit.com/r/CUDA/comments/1tg3vym/built_an_open_source_gpu_bottleneck_analyzer_for/

Fournex is a newly released open-source tool designed to help developers identify and resolve GPU bottlenecks in PyTorch/CUDA applications. By leveraging Nsight Compute output, Fournex provides specific, evidence-backed optimization suggestions for CUDA kernels. This eliminates the guesswork often involved in performance tuning, translating raw profiling data into actionable insights. The tool aims to make GPU optimization more accessible, offering clear guidance on how to improve the efficiency of deep learning workloads and other GPU-intensive tasks.

Fournex operates by taking Nsight Compute’s detailed kernel profiling results and applying intelligent analysis to pinpoint performance limitations. This could include issues like suboptimal memory access patterns, low occupancy, or inefficient instruction utilization. Developers can feed their Nsight Compute .ncu-rep files into Fournex to receive a report detailing specific areas for improvement, complete with explanations of why certain bottlenecks occur and potential strategies for mitigation. This level of granular feedback is invaluable for engineers looking to extract maximum performance from their NVIDIA GPUs without needing to be an expert in low-level CUDA optimization. Its open-source nature encourages community contributions and transparency, fostering a collaborative environment for GPU performance improvement.

Comment: This looks like a lifesaver for debugging slow PyTorch models. Nsight Compute can be overwhelming, so having an automated analyzer to point out specific CUDA kernel bottlenecks and suggest fixes is incredibly useful. Definitely worth integrating into my performance tuning workflow.

Nvidia’s Rubin AI platform will reportedly demand more DRAM than Apple and Samsung combined (r/nvidia)

Source: https://reddit.com/r/nvidia/comments/1tgssdd/nvidias_rubin_ai_platform_will_reportedly_demand/

A recent report from Citrini Research projects that NVIDIA’s upcoming Rubin AI platform will require an unprecedented amount of DRAM, potentially exceeding the combined annual demand of tech giants Apple and Samsung. This massive memory requirement underscores the escalating need for high-bandwidth memory (HBM) in next-generation AI accelerators, driven by increasingly complex large language models and other compute-intensive AI workloads. The Rubin platform, expected to succeed the Blackwell architecture, is poised to push the boundaries of memory capacity and bandwidth, indicating a significant shift in silicon roadmap priorities towards massive data throughput.

This forecast has profound implications for the global memory market, suggesting continued pressure on HBM supply and potentially higher prices. NVIDIA’s strategy with Rubin appears to center on providing unparalleled memory resources to enable models that demand terabytes of data during training and inference. Such a monumental increase in DRAM consumption reflects a future where AI systems are not just faster, but also capable of handling vastly larger datasets and model parameters directly within GPU memory. For developers, this signifies the potential for even larger models to be trained and deployed with reduced reliance on CPU-side memory, further accelerating AI research and deployment. The architectural design of Rubin will likely focus heavily on optimizing memory access and inter-GPU communication to leverage this immense HBM capacity effectively.

Comment: This puts the sheer scale of future AI hardware into perspective. The Rubin platform’s memory demand signals a monumental shift, meaning HBM supply will be critical for anyone planning future large-scale AI infrastructure. Get ready for even tighter memory markets.

Qwen 3.6 27B on 24GB VRAM setup: backend comparisons, quant choice and settings (llama.cpp, ik_llama.cpp, BeeLlama, vllm) (r/LocalLLaMA)

Source: https://reddit.com/r/LocalLLaMA/comments/1tgis7s/qwen_36_27b_on_24gb_vram_setup_backend/

This detailed analysis explores optimal configurations for running the Qwen 3.6 27B large language model on consumer-grade hardware, specifically a 24GB RTX 3090 GPU. The report provides a comparative benchmark across various inference backends, including llama.cpp, ik_llama.cpp, BeeLlama, and vllm, highlighting their VRAM efficiency and performance characteristics. Key findings indicate that ik_llama.cpp combined with Qwen3.6-27B-MTP-IQ4_KS.gguf quantization offers the best setup for maximizing context length and throughput, achieving a 156k context window with q8_0/q8_0 KV cache settings. This hands-on investigation provides concrete data for local LLM enthusiasts and practitioners seeking to optimize their inference pipelines for constrained VRAM environments.

The methodology delves into different quantization choices and their impact on both VRAM consumption and output quality. It specifically explores the MTP (Multi-Turn Prediction) implementation within llama.cpp and its VRAM requirements, along with strategies for quantizing the MTP KV cache to further reduce memory footprint. Benchmarks included a ~5.9k prompt yielding 1k output tokens, offering a realistic scenario for performance evaluation. This technical deep-dive is invaluable for understanding how various backend optimizations, such as KV cache quantization and specific model formats (e.g., .gguf), can significantly improve the feasibility of running large models on more accessible hardware. It provides a practical roadmap for achieving high performance and large context windows within typical consumer GPU limitations.

Comment: This is exactly the kind of VRAM optimization guide I need for running larger models locally. The comparison of llama.cpp variants and quantization settings for Qwen 3.6 27B on a 24GB card is gold, especially the details on MTP KV cache quantization for longer contexts.