File size: 28,369 Bytes
8bc8124
6f8eac4
 
 
 
76adf06
 
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76adf06
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76adf06
6f8eac4
7215d2b
 
 
 
 
 
 
 
 
76adf06
 
 
 
 
6f8eac4
764fe7f
6f8eac4
7215d2b
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fda6bf9
 
 
6f8eac4
 
 
 
 
7215d2b
 
 
6f8eac4
7215d2b
6f8eac4
 
 
 
 
 
7215d2b
 
 
 
 
 
 
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7215d2b
6f8eac4
 
 
76adf06
 
6f8eac4
7215d2b
6f8eac4
7215d2b
fda6bf9
 
 
 
 
 
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
185aad5
 
 
6f8eac4
185aad5
6f8eac4
 
 
 
 
 
923558a
 
8bc8124
 
923558a
 
 
 
 
8bc8124
 
923558a
 
1079f5b
 
 
 
cb00666
1079f5b
cb00666
1079f5b
 
6f8eac4
 
76adf06
 
 
 
 
 
 
 
 
 
 
 
 
6f8eac4
7215d2b
 
 
 
 
 
 
 
 
 
 
 
 
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
1603d8f
 
 
 
 
6f8eac4
 
 
7215d2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f427738
 
6f8eac4
 
 
 
 
 
 
 
db8c698
 
6f8eac4
0cd774a
6f8eac4
 
 
1079f5b
6f8eac4
a0d61cb
 
 
54673e6
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0d61cb
 
1079f5b
 
16f8498
 
 
 
1079f5b
cb00666
1079f5b
cb00666
1079f5b
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bef3c2a
 
 
 
 
 
 
6f8eac4
f427738
76adf06
830ca4a
 
 
6f8eac4
 
 
76adf06
 
 
 
16f8498
 
cb00666
1079f5b
cb00666
 
 
 
a0d61cb
 
 
 
1079f5b
6f8eac4
 
 
c5fce40
3a31d01
bef3c2a
 
 
c5fce40
 
 
bef3c2a
6f8eac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# deploy: 20261020-a3cb7f2-captions (simready-assets)
# GPU variant of the single-container MVP image for Hugging Face Spaces
# (Docker SDK): everything docker/Dockerfile.hf-space has (OpenSearch + Neo4j
# + Redis + deepsearch-api + asset-graph-service + nginx under supervisord),
# plus a *local* siglip2-triton process instead of routing SigLIP2 embeddings
# to NVCF β€” see docs/development.md#hf-space-design-notes
# for the decisions behind this file. Meant to be deployed to an HF Space running
# on a GPU hardware tier (T4 medium β€” see pipeline/hf_space.py's
# request_space_hardware call for the gpu variant); docker/Dockerfile.hf-space
# remains the free/cpu-basic, NVCF-backed variant β€” this is an additional
# deploy target, not a replacement (`SIGLIP2_BACKEND=nvcf` is unaffected).
#
# Builds the Triton half of this image the same way
# ../usdsearch/docker/Dockerfile.siglip2-triton does β€” FROM the bare, public
# nvcr.io/nvidia/tritonserver image, model_repo/ COPYed in from
# docker/hf-space/siglip2-model/ (vendored into *this* repo by
# scripts/extract_siglip2_model.sh, since HF's remote builder can reach
# neither ../usdsearch nor any private registry) β€” rather than FROM the
# prepackaged nvcr.io/nvidia/usdsearch/siglip2-triton image an earlier
# revision of this file used. Verified live why that prepackaged image was
# dropped: its published 1.4.0 tag bakes **FP32** weights (vision 4.4 GB +
# text 2.7 GB, inspected inside a pulled copy) β€” at this model's baked
# `instance_group: 2`, that's ~14 GB of resident GPU weight memory alone,
# and ~18 GB observed live in `nvidia-smi`, blowing past a T4's 16 GB VRAM.
# ../usdsearch's *current* services/siglip2-triton/model_repo checkout is
# already FP16-exported (vision 2.2 GB + text 1.4 GB β€” half the size, ~7 GB
# at instance_group: 2, comfortably under 16 GB) but was never republished
# under that NGC tag β€” vendoring this checkout's own model_repo directly
# sidesteps waiting on a new publish. See
# scripts/extract_siglip2_model.sh's own header for the full mechanism.
#
# nvcr.io/nvidia/tritonserver itself (the base, not the prepackaged image)
# is on the same *public* NGC catalog path already verified live this
# project's other public pulls (opensearchproject/opensearch, neo4j) use β€”
# no credentials needed, so Hugging Face's own remote builder can pull it.
#
# VERIFIED LIVE on a real GPU (RTX 4090, local β€” not a T4, but same
# execution path): full build, `docker run --gpus all`, all 6 Triton models
# READY, OpenSearch/Neo4j restore from S3, deepsearch-api + asset-graph-service
# healthy through nginx. GPU memory settled at ~10-11 GB (tritonserver
# ~10.3 GB + a smaller deepsearch-api CUDA-context allocation) β€” comfortably
# under a T4's 16 GB, confirming the FP32->FP16 switch actually fixed the
# budget problem, not just on paper. A real `/v3/deepsearch/search` call
# came back with genuine `search_type: text_to_vector` /
# `field: siglip2-embedding` hits (query "a red chair" correctly ranked
# office-chair assets top) β€” the local siglip2-triton round trip is real,
# not just "the process started". LLM query parsing
# (`/v3/deepsearch/llm_parse/query`) and VLM validation
# (`/v3/deepsearch/vlm_validate/search_result`) were also exercised live
# against the already-deployed NVCF Qwen endpoint
# (config/pipeline.env.srp-staging-controlled's LLM_ENDPOINT_BASE_URL) with
# $NVCF_GENAI_RUN_KEY as USDSEARCH_LLM_API_KEY β€” both returned real model
# output (a structured filter parse; a logprob-based
# `P(Yes)=... from first-token log-probs.` verdict), confirming this
# variant's NVCF-backed LLM/VLM path works the same as the NVCF/cpu-basic
# variant's.

# 3.9.0 rather than the pipeline's 3.8.0: 3.8.0's netty-handler 4.2.16
# (GHSA-c4c3-7fpv-j4q5, Critical) and httpcore5 5.4 jars were flagged by the
# nSpect container scan (2026-10-02). The Space bulk-loads the pipeline's
# NDJSON (pipeline/restore.py) rather than restoring a snapshot, so the
# versions don't need to match. Not named OPENSEARCH_VERSION: the jar patches
# below are tied to this exact version, and the pipeline's own
# OPENSEARCH_VERSION used to be passed through as a build-arg (and is still
# set as a Variable on the existing Spaces, which HF would pass along).
ARG HF_SPACE_OPENSEARCH_VERSION=3.9.0
# 5.26 (LTS) rather than the pipeline's 5.23: 5.23's bundled netty/jackson/
# jetty/shiro jars, APOC's json-smart and its JDK 17.0.12 were all flagged
# High by the nSpect container scan (2026-10-02). Same major version, so the
# pipeline's 5.23 dumps still load (verified locally).
ARG NEO4J_VERSION=5.26.31-community
ARG TRITONSERVER_BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.05-py3
ARG PYTHON_IMAGE=python:3.12-bookworm

FROM opensearchproject/opensearch:${HF_SPACE_OPENSEARCH_VERSION} AS opensearch-src

FROM neo4j:${NEO4J_VERSION} AS neo4j-src

FROM ${TRITONSERVER_BASE_IMAGE}

WORKDIR /workspace
RUN mkdir -p /models

# Vendored model repo (see header comment) β€” ensembles, the Python-backend
# tokenizer/preprocessing models, and the two ONNX encoders' *config.pbtxt*
# only. The encoders' actual weight files (~3.6 GB combined β€” the ONNX
# externa-data blobs under each encoder's own 1/ subdirectory) are
# deliberately NOT baked here: a real `deploy-space-gpu` attempt 403'd with
# "Repository storage limit reached (Max: 1 GB)" β€” HF Space git repos have a
# default 1 GB storage quota, well under what baking these in needs. Instead
# they're restored from the artifact S3 bucket at container startup
# (pipeline/hf_space_seed.py --component=siglip2-model, run by
# supervisord-gpu.conf's [program:siglip2-triton] before tritonserver
# itself execs) β€” the same "runtime restore, not baked" pattern this image
# already uses for the OpenSearch index and Neo4j graph, just for a
# corpus-independent static artifact instead of per-run data. Publish
# weights to that S3 path with scripts/publish_siglip2_model_to_s3.sh
# whenever scripts/extract_siglip2_model.sh refreshes the vendored copy.
COPY docker/hf-space/siglip2-model/model_repo/ensemble_image_model /models/ensemble_image_model
COPY docker/hf-space/siglip2-model/model_repo/ensemble_text_model /models/ensemble_text_model
COPY docker/hf-space/siglip2-model/model_repo/image_preprocessing /models/image_preprocessing
COPY docker/hf-space/siglip2-model/model_repo/siglip2_vision_encoder_onnx/config.pbtxt /models/siglip2_vision_encoder_onnx/config.pbtxt
COPY docker/hf-space/siglip2-model/model_repo/siglip2_text_encoder_onnx/config.pbtxt /models/siglip2_text_encoder_onnx/config.pbtxt
RUN mkdir -p /models/siglip2_vision_encoder_onnx/1 /models/siglip2_text_encoder_onnx/1
COPY docker/hf-space/siglip2-model/model_repo/tokenizer /models/tokenizer
# Owned by uid 1000: [program:siglip2-triton] restores the encoder weights into /models as that user. Some Space
# hardware (rtx-pro-6000, 2026-10-21) runs the container as uid 1000 whatever USER says, so nothing may need root.
RUN chown -R 1000:1000 /models

# Python deps for the tokenizer + image_preprocessing Python-backend models β€”
# resolved from pyproject.toml + uv.lock (the source of truth) at build time,
# same as ../usdsearch/docker/Dockerfile.siglip2-triton itself does, so this
# doesn't need a separately-maintained requirements.txt that can drift.
# uv and virtualenv (below) are pinned so the third-party notice is
# reproducible: scripts/generate_third_party_notice.sh reads both pins from
# this file β€” re-run it after bumping either.
COPY docker/hf-space/siglip2-model/pyproject.toml docker/hf-space/siglip2-model/uv.lock /workspace/
RUN pip install --no-cache-dir uv==0.12.22 \
    && uv export --frozen --no-hashes --directory /workspace -o /workspace/requirements.txt \
    && pip install --no-cache-dir -r /workspace/requirements.txt \
    && rm /workspace/requirements.txt

COPY docker/hf-space/siglip2-model/PACKAGE-LICENSES /PACKAGE-LICENSES

# Attribution for anyone who pulls the image: this project's LICENSE plus the
# third-party notices (scripts/generate_third_party_notice.sh). Root-level in
# both build contexts (this repo for bake, the staged Space tree for HF);
# licenses/ stays next to the notice so its relative links resolve.
COPY LICENSE THIRD_PARTY_NOTICE.md /licenses/
COPY licenses /licenses/licenses

# Same two size/vulnerability trims ../usdsearch/docker/Dockerfile.siglip2-triton
# itself applies to this exact base image β€” carried over verbatim rather than
# dropped, since they're cheap and free up real space (~1 GB+ for Nsight
# Systems alone) on top of the FP16 savings above.
RUN rm -rf /usr/local/cuda-13.2/NsightSystems-cli-2026.2.1

USER root

# Non-root runtime uid/gid 1000, same convention docker/Dockerfile.hf-space
# uses for every data-writing process in that image (matches
# opensearchproject/opensearch's own "opensearch" user, 1000:1000). This
# base image is Ubuntu 24.04 ("noble") β€” not verified live whether uid 1000
# is already taken by a user the NGC tritonserver image itself creates, so
# this reuses it (rather than erroring) if it is: --chown=1000:1000 below
# only cares that the numeric id resolves consistently, not which username
# owns it.
RUN if id -u 1000 >/dev/null 2>&1; then \
        echo "uid 1000 already exists as $(getent passwd 1000 | cut -d: -f1) β€” reusing it"; \
    else \
        groupadd -g 1000 nvs && useradd -u 1000 -g 1000 -m -d /home/nvs -s /usr/sbin/nologin nvs; \
    fi

# python3.12-venv is requested
# explicitly (Ubuntu splits the venv module out of the base python3.12
# package on some images) since this base is noble rather than bookworm and
# hasn't been checked for what's preinstalled.
# libssl3t64 and the virtualenv upgrade mirror usdsearch 1.5.0-rc.7's
# docker/Dockerfile.siglip2-triton vulnerability fixes for this same base.
RUN apt-get update && apt-get install -y --no-install-recommends \
        redis-server supervisor nginx curl python3.12-venv openssl libssl3t64 \
    && rm -rf /var/lib/apt/lists/* \
    && pip install --no-cache-dir --upgrade virtualenv==21.14.5 \
    && rm -f /etc/nginx/sites-enabled/default \
    && sed -i 's|^pid .*|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf \
    && chown -R 1000:1000 /etc/nginx/conf.d /var/lib/nginx /var/log/nginx
# nginx runs as uid 1000 (supervisord-gpu.conf): the pid file moves to /tmp, and the rendered config, temp and log
# dirs belong to that user. Some Space hardware (rtx-pro-6000, 2026-10-21) runs the container as uid 1000 whatever
# USER says, and nginx with user=root never started there (supervisord: "Can't drop privilege as nonroot user").

# The app venv: same /app/.venv path docker/Dockerfile.hf-space and
# docker/hf-space/supervisord*.conf both hardcode. Installed from the same
# repacked usdsearch wheels + constraints.txt as the NVCF variant β€” see that
# Dockerfile's header comment for how wheels/ is produced and refreshed.
# Upstream's own docker/Dockerfile.usdsearch compiles these wheels in a
# python:3.12-bookworm builder stage but runs them in a
# nvcr.io/nvidia/base/ubuntu:noble-20260217 *runtime* stage β€” the same OS
# family as this Triton image's own base β€” so installing them here follows
# upstream's own production direction (older-glibc build -> newer-glibc
# run), not a new cross-distro risk. Not yet verified live in this repo,
# though (see header comment).
COPY docker/hf-space/wheels/ /tmp/usdsearch-wheels/
# env -u PYTHONPATH: with the Triton base image's PYTHONPATH (DALI's wheel dir) visible, pip treated
# packages found there as installed and the venv came out without six (2026-09-29).
# The venv APIs start without PYTHONPATH (supervisord-gpu.conf), so the venv must be complete.
RUN python3.12 -m venv /app/.venv \
    && env -u PYTHONPATH /app/.venv/bin/pip install --no-cache-dir \
        --find-links /tmp/usdsearch-wheels \
        --constraint /tmp/usdsearch-wheels/constraints.txt \
        /tmp/usdsearch-wheels/*.whl \
    && rm -rf /tmp/usdsearch-wheels \
    && chown -R 1000:1000 /app/.venv

# The vision checks' prepared asset views on disk (docker/hf-space/check_image_cache.py), on while the
# CHECK_IMAGE_CACHE_DIR Space Variable is set (pipeline/config.py). The .pth loads it in every venv python. The
# build fails unless it wraps the vendored llm_client and cached views come back byte-identical (--self-test), as PNG
# (the default) and as JPEG (USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg, the Space Variable prod runs with).
COPY docker/hf-space/check_image_cache.py docker/hf-space/check_image_cache.pth /tmp/check-image-hook/
RUN SP=$(env -u PYTHONPATH /app/.venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') \
    && install -m 644 /tmp/check-image-hook/check_image_cache.py /tmp/check-image-hook/check_image_cache.pth "$SP/" \
    && rm -rf /tmp/check-image-hook \
    && env -u PYTHONPATH CHECK_IMAGE_CACHE_DIR=/tmp/check-image-selftest /app/.venv/bin/python -m check_image_cache --self-test \
    && env -u PYTHONPATH CHECK_IMAGE_CACHE_DIR=/tmp/check-image-selftest USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg \
        /app/.venv/bin/python -m check_image_cache --self-test \
    && rm -rf /tmp/check-image-selftest

# hf-storage-api β€” see docker/Dockerfile.hf-space's own copy of this block
# for the full explanation (identical here; this variant serves the same
# corpus/storage backend, just with a different SigLIP2 backend).
COPY docker/hf-space/hf-storage-api/hf_storage_api /app/hf-storage-api/src/hf_storage_api
RUN GRPCIO_VERSION=$(env -u PYTHONPATH /app/.venv/bin/pip show grpcio | sed -n 's/^Version: //p') \
    && env -u PYTHONPATH /app/.venv/bin/pip install --no-cache-dir "grpcio-health-checking==${GRPCIO_VERSION}" \
    && env PYTHONPATH=/app/hf-storage-api/src /app/.venv/bin/python -c "import hf_storage_api.server" \
    && chown -R 1000:1000 /app/hf-storage-api

COPY --from=opensearch-src --chown=1000:1000 /usr/share/opensearch /usr/share/opensearch

# Keep only opensearch-knn: deepsearch-api's hybrid search fuses results
# client-side and the restored index uses only core analyzers + index.knn.
# The other bundled plugins carry netty/httpcore5 jars flagged High by the
# nSpect container scan (2026-10-02). Several passes because a plugin can't
# be removed while another still extends it. With opensearch-security gone,
# the entrypoint skips its DISABLE_SECURITY_PLUGIN handling.
RUN cd /usr/share/opensearch \
    && for pass in 1 2 3; do \
        for p in $(ls plugins); do \
            [ "$p" = opensearch-knn ] || bin/opensearch-plugin remove "$p" >/dev/null 2>&1 || true; \
        done; \
    done \
    && [ "$(ls plugins)" = opensearch-knn ] || { echo "plugins left: $(ls plugins)" >&2; exit 1; }

# Jackson jars no OpenSearch release fixes yet (nSpect, 2026-10-02):
# - lib/jackson-core 3.2.2 -> 3.2.3 (GHSA-7hhh-6rmp-j9qf), a patch-level
#   drop-in from Maven Central, sha256-pinned. The old jar is removed by exact
#   name, so a base bump that already changed it fails here β€” re-check then.
# - modules/ingest-geoip (jackson-databind/-core 2.22.2: GHSA-cxp5-3px4-pw24,
#   GHSA-wv8q-qhhj-9h54, ...) removed: no ingest pipeline uses GeoIP.
RUN cd /usr/share/opensearch \
    && rm -r modules/ingest-geoip lib/jackson-core-3.2.2.jar \
    && curl -fsSL -o lib/jackson-core-3.2.3.jar \
        https://repo1.maven.org/maven2/tools/jackson/core/jackson-core/3.2.3/jackson-core-3.2.3.jar \
    && echo "a0c17e7e665a5e95c82f5fb45c0fec2ae3dc556c80f0016452c9c84cd23f3817  lib/jackson-core-3.2.3.jar" | sha256sum -c - \
    && chown 1000:1000 lib/jackson-core-3.2.3.jar

# See docker/Dockerfile.hf-space's own (much longer) comment on this same
# block for the full k-NN/faiss library-path story β€” copied verbatim here
# since it's the identical opensearchproject/opensearch source image and the
# identical cross-image-copy cause.
ENV OPENSEARCH_HOME=/usr/share/opensearch \
    OPENSEARCH_PATH_CONF=/usr/share/opensearch/config \
    PATH="/usr/share/opensearch/bin:/app/.venv/bin:${PATH}" \
    LD_LIBRARY_PATH=/usr/share/opensearch/plugins/opensearch-knn/lib \
    discovery.type=single-node \
    DISABLE_SECURITY_PLUGIN=true \
    DISABLE_INSTALL_DEMO_CONFIG=true \
    bootstrap.memory_lock=false \
    OPENSEARCH_JAVA_OPTS="-Xms1g -Xmx1g -Djava.library.path=/usr/share/opensearch/plugins/opensearch-knn/lib"

# A Category pick sends one filter value per leaf, two Lucene clauses each, so
# the default cap (1,024) refused 513+ values with HTTP 400 (2026-10-19:
# Industrial + Residential + OEM = 519). 8192 takes ~4,000 values.
ENV indices.query.bool.max_clause_count=8192

COPY --from=neo4j-src /opt/java/openjdk /opt/java/openjdk
COPY --from=neo4j-src /var/lib/neo4j /var/lib/neo4j

# Jackson jars no Neo4j release fixes yet (nSpect, 2026-10-02 β€” 2026.09.0
# ships the same ones):
# - lib/jackson-core/-databind 2.22.2 -> 2.22.3 (GHSA-p6pp-m3f8-5c89,
#   GHSA-7hhh-6rmp-j9qf, GHSA-cxp5-3px4-pw24, GHSA-wv8q-qhhj-9h54), patch-level
#   drop-ins from Maven Central, sha256-pinned; old jars removed by exact name.
# - lib/parquet-jackson removed: it shades jackson 2.22.1 (same advisories)
#   and has no fixed release. Only neo4j-admin's Parquet *import* uses it;
#   this image only runs `neo4j-admin database load` and the server.
RUN cd /var/lib/neo4j/lib \
    && rm jackson-core-2.22.2.jar jackson-databind-2.22.2.jar parquet-jackson-1.18.0.jar \
    && curl -fsSL -o jackson-core-2.22.3.jar \
        https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.22.3/jackson-core-2.22.3.jar \
    && curl -fsSL -o jackson-databind-2.22.3.jar \
        https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.22.3/jackson-databind-2.22.3.jar \
    && printf '%s  %s\n' \
        8a501126a385b25841915d839508f8a66e2a0dbc8a6709d055ef3b3e852b094c jackson-core-2.22.3.jar \
        556db5439e206114346043f68d200497dc96a0bca62a360a81784092ebd0e0a9 jackson-databind-2.22.3.jar \
        | sha256sum -c -

RUN mkdir -p /data /logs && chmod 777 /data /logs

RUN cp /var/lib/neo4j/labs/apoc-*-core.jar /var/lib/neo4j/plugins/

RUN { \
    echo "dbms.security.auth_enabled=false"; \
    echo "server.memory.heap.initial_size=1G"; \
    echo "server.memory.heap.max_size=1G"; \
    echo "server.memory.pagecache.size=512M"; \
    } >> /var/lib/neo4j/conf/neo4j.conf

ENV NEO4J_HOME=/var/lib/neo4j \
    PATH="/var/lib/neo4j/bin:/opt/java/openjdk/bin:${PATH}"

COPY docker/hf-space/logging.yml /logging.yml
COPY docker/hf-space/search_fields.yaml /app/search_fields.yaml
# The query parser's system prompt β€” same as docker/Dockerfile.hf-space, see its comment.
COPY docker/hf-space/llm_parse_prompt.txt /app/llm_parse_prompt.txt

# The GPU-variant supervisord config β€” same six programs as
# docker/hf-space/supervisord.conf plus [program:siglip2-triton], and
# deepsearch-api's own command additionally waits on Triton's readiness
# before execing uvicorn. See that file's header for the port-collision
# reasoning (Triton's default HTTP port 8000 clashes with deepsearch-api's
# own uvicorn; remapped to 8003 here).
COPY docker/hf-space/supervisord-gpu.conf /etc/supervisor/conf.d/hf-space.conf
# nginx's optional readiness gate (LIBRARY_MEDIA_GATE; see the script's header).
COPY docker/hf-space/media-gate.sh /usr/local/bin/media-gate.sh

COPY docker/hf-space/nginx.conf.template /etc/nginx/hf-space.conf.template

COPY services/explorer/dist /usr/share/nginx/explorer


COPY --chown=1000:1000 pipeline /app/hf-space-pipeline/pipeline
# package-download (supervisord's program of that name) runs from this copy on the
# venv deepsearch-api already brought (fastapi, uvicorn, grpcio, the storage_api
# stubs, search_utils): fail the build, not the Space, if that ever stops importing.
RUN cd /app/hf-space-pipeline && env -u PYTHONPATH /app/.venv/bin/python -c "import pipeline.package_download.app, pipeline.package_download.source, pipeline.package_download.sizes, search_utils.url_mapping"

ARG ARTIFACT_S3_URI
ARG ARTIFACT_S3_REGION=us-east-2
ARG CORPUS_NAME
ENV ARTIFACT_S3_URI=${ARTIFACT_S3_URI} \
    ARTIFACT_S3_REGION=${ARTIFACT_S3_REGION} \
    CORPUS_NAME=${CORPUS_NAME}

# ─── deepsearch-api config baked for this corpus/run ────────────────────────
# Same as docker/Dockerfile.hf-space's equivalent block, minus every
# NVCF-specific ARG/env (SIGLIP2_NVCF_ENDPOINT/FUNCTION_ID and their
# downstream SIGLIP2_TRITON_SERVER_* values) β€” this variant never talks to
# NVCF at all. SIGLIP2_TRITON_SERVER_URL instead points at the local
# siglip2-triton process's gRPC port (8001, loopback-only β€” nginx remains
# the sole externally-proxied port, same as every other backend process in
# this image), with no SSL and no auth headers, since nothing external ever
# reaches it.
ARG ES_NAME
ARG S3_STORAGE_BUCKET_NAME
ARG S3_STORAGE_REGION_NAME=us-west-2
ARG STORAGE_BACKEND_TYPE=s3
# DOWNLOAD_ENABLED / HF_STORAGE_API_TOKEN / SUBMIT_CONTENT_URL / STORAGE_API_* / DOWNLOAD_* /
# PACKAGE_DOWNLOAD_* β€” see
# docker/Dockerfile.hf-space's own copies of these comments for the full
# explanation.
# USDSEARCH_URL_MAPPING_MODE/_RULES β€” hf_space_build_args() derives them from
# the repos (and pinned revisions) this run indexed; see url_mapping_rules().
ARG USDSEARCH_URL_MAPPING_MODE=off
ARG USDSEARCH_URL_MAPPING_RULES=[]
ARG DOWNLOAD_ENABLED=false
ENV HF_STORAGE_API_TOKEN= \
    DOWNLOAD_ENABLED=${DOWNLOAD_ENABLED} \
    SUBMIT_CONTENT_URL=https://huggingface.co/datasets/nvidia/simready-central/discussions \
    STORAGE_BACKEND_TYPE=${STORAGE_BACKEND_TYPE} \
    S3_STORAGE_BUCKET_NAME=${S3_STORAGE_BUCKET_NAME} \
    S3_STORAGE_REGION_NAME=${S3_STORAGE_REGION_NAME} \
    opensearch_host=http://localhost:9200 \
    opensearch_index_name=${ES_NAME}-ver5.0 \
    opensearch_image_cache_index_name=${ES_NAME}-ver4.0-image-cache \
    opensearch_use_ssl=false \
    REDIS_URL=redis://localhost:6379 \
    DEEPSEARCH_BACKEND_ASSET_GRAPH_SERVICE_URL=http://127.0.0.1:8010 \
    ASSET_GRAPH_SERVICE_ENDPOINT=http://127.0.0.1:8010 \
    ENABLE_ACCESS_VERIFICATION=False \
    STORAGE_REQUIRE_AUTH=False \
    VERIFY_ASSET_EXISTENCE=False \
    verify_access=false \
    ASSERT_ADMIN_USER=false \
    USDSEARCH_VISION_VALIDATION_ENABLED=false \
    USDSEARCH_LLM_PARSING_ENABLED=false \
    # Validation limits are per deepsearch-api process: 32 checks in flight and a 31k-verdict cache in
    # each of the 16 processes (DEEPSEARCH_API_PROCESSES below), 512 and ~500k in all (Redis keeps every verdict
    # anyway). One search's page runs at most 16 at once (usdsearch's BATCH_CONCURRENCY). 2026-10-20 lab, l4x4
    # shape with the check-image cache: 16 per process capped the engine side at ~155 checks/s (p95 of all badges
    # 14 s at 80 users); 32 held 3.2 s up to 240 users.
    USDSEARCH_VISION_VALIDATION_MAX_CONCURRENT=32 \
    USDSEARCH_VISION_VALIDATION_CACHE_SIZE=31250 \
    USDSEARCH_LLM_PARSING_FIELDS_FILEPATH=/app/search_fields.yaml \
    USDSEARCH_LLM_PARSING_PROMPT_FILEPATH=/app/llm_parse_prompt.txt \
    # Business telemetry β€” same as docker/Dockerfile.hf-space, see its comment.
    OTEL_SDK_DISABLED=false \
    USE_SEARCH_TELEMETRY=true \
    OTEL_TRACES_EXPORTER=false \
    BACKEND_CONFIG_STR="backend_name: es-sample" \
    DEFAULT_CUTOFF_THRESHOLD=0 \
    HYBRID_SEARCH_SCORING_CONFIG_VECTOR_FIELDS="{\"siglip2-embedding.embedding\":{\"enabled\":true,\"weight\":1,\"field_name\":\"siglip2-embedding.embedding\",\"dimension\":1536}}" \
    # Text-leg default β€” same as docker/Dockerfile.hf-space, see its comment.
    HYBRID_SEARCH_HYBRID_TEXT_CONFIG_WEIGHT=0.9 \
    HYBRID_SEARCH_HYBRID_TEXT_CONFIG_CROSS_FIELD_OPERATOR=or \
    HYBRID_SEARCH_HYBRID_TEXT_CONFIG_FIELDS="[{\"field\":\"name\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name.simple\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name.standard\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"usd_properties.value\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"usd_properties.key\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"tags.tag\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"tags.value\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path.tree\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path.tree_reversed\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"__VISION_METADATA_FIELDS__\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name\",\"nested\":false,\"enabled\":true,\"weight\":5,\"match_type\":\"exact\",\"analyzer\":\"keyword\",\"wildcard\":false},{\"field\":\"name.standard\",\"nested\":false,\"enabled\":true,\"weight\":5,\"match_type\":\"exact\",\"analyzer\":\"keyword\",\"wildcard\":false}]" \
    USDSEARCH_URL_MAPPING_MODE=${USDSEARCH_URL_MAPPING_MODE} \
    USDSEARCH_URL_MAPPING_RULES=${USDSEARCH_URL_MAPPING_RULES} \
    STORAGE_API_GRPC_ENDPOINT=127.0.0.1:50051 \
    STORAGE_API_SSL=false \
    STORAGE_API_IGNORE_FILEFOLDER_API=true \
    DOWNLOAD_MAX_CONCURRENT=2 \
    DOWNLOAD_QUEUE_TIMEOUT=120 \
    DOWNLOAD_MAX_BUNDLE_BYTES=2000000000 \
    PACKAGE_DOWNLOAD_PORT=8020 \
    PACKAGE_DOWNLOAD_MAX_CONCURRENT=4 \
    PACKAGE_DOWNLOAD_QUEUE_TIMEOUT=120 \
    PACKAGE_DOWNLOAD_MAX_BUNDLE_BYTES=2000000000 \
    STORAGE_API_THUMBNAIL_METADATA_FIELDS="[\"hf:thumbnail_url\"]" \
    SIGLIP2_TRITON_SERVER_URL=localhost:8001 \
    TRITON_SERVER_URL=localhost:8001 \
    SIGLIP2_TRITON_SERVER_SSL=false \
    # deepsearch-api processes, ONE uvicorn worker each, behind nginx's least_conn
    # upstream (supervisord-gpu.conf, nginx.conf.template). 16 for l4x4 (48 vCPU):
    # 2026-10-20 lab, l4x4 shape, 32 checks each: 16 processes held p95 of all badges at
    # 3.2 s up to 240 users (6.3 s at 300, CPU-bound); 6 processes reached ~100 users
    # with the CPU under 30 %. On a10g-large (12 vCPU) use 6: 8 cost ~20 % more CPU there. Must equal supervisord-gpu.conf's
    # numprocs: nginx renders its upstream from this, so never set it as a Space
    # Variable. DEEPSEARCH_API_WORKERS is no longer read (a leftover Space Variable
    # of that name does nothing).
    DEEPSEARCH_API_PROCESSES=16 \
    PORT=7860

ARG RUN_ID
ARG CORPUS_SHA256
ARG USDSEARCH_GIT_SHA
LABEL org.opencontainers.image.title="usdsearch-hf-space-simready-central-gpu" \
      org.opencontainers.image.description="Single-container MVP (OpenSearch+Neo4j+Redis+deepsearch-api, SigLIP2 via local Triton on GPU) for Hugging Face Spaces" \
      com.nvidia.usdsearch.run_id="${RUN_ID}" \
      com.nvidia.usdsearch.corpus_sha256="${CORPUS_SHA256}" \
      com.nvidia.usdsearch.usdsearch_git_sha="${USDSEARCH_GIT_SHA}"

# Longer start-period than docker/Dockerfile.hf-space's 10m: this variant
# additionally waits on Triton loading ~3.6 GB of ONNX weights onto the GPU
# before deepsearch-api's own uvicorn execs (see supervisord-gpu.conf).
# Verified live against a small (1488-asset) dev corpus: Triton model load
# took ~10s and the whole stack was healthy within ~20s total β€” 15m is
# deliberately still a generous upper bound, not a tight one, since a larger
# production corpus's OpenSearch bulk load / Neo4j offline load (the same
# restore path docker/Dockerfile.hf-space already budgets 10m for) can take
# much longer than this dev corpus did.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15m --retries=3 \
    CMD curl -sf "http://localhost:${PORT}/health" || exit 1

EXPOSE 7860
ENTRYPOINT ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/hf-space.conf"]