Skip to main content
← All writingKylan Thomson

Generative AI · Content Operations

Building AI Image Pipelines Around Search Demand Instead of Prompt Quality

On a marketplace that pays per download, an image nobody can find is worth nothing. The pipeline that earned 5,110 downloads starts from a search-volume export, not from a prompt.

Published
Reading time
9 minutes
Project
AI Stock Image Factory case study

Stock marketplaces pay per download, and downloads follow search. So the pipeline behind my Adobe Stock portfolio does not start from a prompt, a mood board, or a style. It starts from a spreadsheet of what people typed into a search box last month, and it treats the picture as the cheap part.

That inversion is the whole idea, and it is worth stating why it is not obvious. Image models made images nearly free, which is exactly why image quality stopped being the thing that sells. On a marketplace, an asset nobody can find earns nothing, and an asset that ranks for a term with real demand earns whether or not it is the best picture on the page. The engineering budget should follow the money. In this system it went into keyword selection and metadata, and the 5,110 downloads that resulted are the argument.

This piece covers the mechanism, what the demand data actually looks like, and, because it is the more useful half, what I got wrong in the first version and fixed in the rewrite.

Sample by demand, do not work a list

The input is a keyword export with a monthly search volume per term. The naive approach is to sort by volume and work down the list. The problem is that the list is enormous and the head is where the competition is: every contributor with the same export is generating for the top hundred terms too. Working top-down over-produces for the head and never reaches the tail, and the tail is where a single well-tagged asset can own a query.

Instead, each asset draws its target keyword at random with probability proportional to search volume, after dropping anything below a minimum demand floor. Sampling covers the whole distribution while still spending most of the budget where the buyers are. The rewrite adds one refinement: the weight is a commercial score, volume boosted by the term's cost-per-click (capped so service-industry terms cannot dominate) and nudged up for commercial intent, and the whole distribution is flattened slightly with a temperature so the head does not swallow the batch.

adjusted_weights = np.power(df["Weight"].values, 1.0 / diversity_factor)
adjusted_weights = adjusted_weights / adjusted_weights.sum()
sel = df.sample(n=n, weights=adjusted_weights, replace=False, random_state=seed)
main.py (2025 rewrite): a temperature-scaled draw, without replacement within a run.

What demand actually looks like

The corpus the rewrite samples from is 18,909 keywords unified from 65 topic exports, and it is about as skewed as these things get. The median keyword is searched 110 times a month. The mean is 598, pulled up by a handful of terms in the hundreds of thousands. Just 329 keywords, under two percent of the corpus, account for half of all monthly search volume.

SHARE OF ALL MONTHLY SEARCHES, 18,909 KEYWORDSTop 1%Top 1% of keywords (189 terms): 44.6% of monthly search volume45%Top 5%Top 5% of keywords (945 terms): 62.0% of monthly search volume62%Top 10%Top 10% of keywords (1,891 terms): 71.3% of monthly search volume71%Top 25%Top 25% of keywords (4,727 terms): 85.2% of monthly search volume85%Top 50%Top 50% of keywords (9,454 terms): 96.0% of monthly search volume96%
View as table
Concentration of search volume
SliceKeywordsShare of volume
Top 1%18944.6%
Top 5%94562.0%
Top 10%1,89171.3%
Top 25%4,72785.2%
Top 50%9,45496.0%
Share of total monthly search volume held by the top slices of the 18,909-keyword corpus. The top one percent of terms carry 45 percent of all searches, the top ten percent carry 71 percent. A uniform sampler would spend 90 percent of its budget on the 29 percent of demand in the tail.

The sampler does bite. Joining the 1,417 assets the rewrite produced back to the corpus, the median keyword actually selected had 880 monthly searches, which is the corpus's 90th percentile: an eight-fold lift over the median term. A fifth of assets targeted top-one-percent terms and half targeted top-ten-percent terms, while the tail still received real coverage.

  • Monthly searches at each percentile (log scale)
101001,00010,000median keyword actually selected: 880 (p90)p10: 30 searches per month30p10p30: 40 searches per month40p30p50: 110 searches per month110p50p70: 320 searches per month320p70p90: 880 searches per month880p90p95: 1,600 searches per month1,600p95p99: 5,400 searches per month5,400p99
Monthly search volume at percentiles of the corpus, on a log scale. The dashed line marks the median volume of the keywords the demand-weighted sampler actually chose: 880, sitting at the corpus's 90th percentile.

Metadata is the product

On a stock marketplace the image is the good but the keywords are the storefront. The marketplace weights the earliest terms in a keyword list far more heavily than the rest, which turns metadata from a labelling problem into a ranking problem. Most of the engineering in the pipeline is here.

  1. Limits are enforced in code, not requested in the prompt. The title is cut to the marketplace's length, backing off to a word boundary; keywords are deduplicated after Unicode normalisation, padded from a filler list up to the floor, and hard-trimmed at the cap. A prompt that asks for 45 to 60 keywords gets 45 to 60 keywords about three quarters of the time. The other quarter is why the code checks.
  2. The target term goes first. The keyword that motivated the asset is forced into the leading slot, and the required disclosure term is inserted at a fixed position near the top, because position is weight.
  3. Re-rank by similarity to the target. Each candidate keyword is embedded and scored by cosine similarity against the target term, then sorted, so the terms closest to actual search intent land in the weighted leading slots. This began as a locally loaded transformer and moved to a hosted embedding endpoint, which costs a rounding error per asset and takes gigabytes out of the deployable image.
  4. Filter the model's tells, not just the content.The banned-term list covers the obvious marketplace-prohibited vocabulary, brands, artists' names, and property that needs a release. It also catches the failure mode where a model narrates instead of answering and emits “Here are the keywords for your image:” straight into a keyword field. The rewrite replaces the literal strings with a preamble regex and a length guard, which is the version I would copy.

What the first version got wrong

The pipeline exists in two generations, and the honest version of this article has to say what the 2024 one did badly, because it is the one whose assets sold. Going back through it with the rewrite's eyes:

  • The embedding re-ranker was inert.The code was correct and sat inside a bare exception handler. A NumPy version conflict made the local transformer raise on every call, the handler swallowed it, and the keywords shipped in the arbitrary order a Python set gave them. The 2026 rewrite's docstring records the diagnosis. The hosted version computes cosine in plain Python with no NumPy, precisely so there is nothing to break.
  • Limits were requested, not enforced. Twelve percent of the shipped manifest rows carried more keywords than the marketplace allows, one with 119. Nothing in the code capped them.
  • The category was random. A weighted draw over three category codes, unrelated to the image. The rewrite routes category from the keyword with a regex fallback, which is at least about the content, and it still collapses to the Technology category 85 percent of the time on a technology-heavy corpus.
  • The style names were never captured.A one-line parsing bug stored the entire five-thousand-character prompt in the style column instead of the style's name, so the provenance the manifest was supposed to carry was there and unusable.

None of those stopped the assets from selling, which is the uncomfortable lesson. Demand-weighted keyword selection and a target-first keyword list were enough. The clever part, the re-ranker, contributed nothing for a year because it was silently broken, and I did not find out until a rewrite made me read the logs.

Free-text style fields do not give you variety

The 2024 pipeline held a library of 72 named house styles, fixed core instructions with a style layered on top, one drawn per image. The rewrite experimented with letting the language model choose a style and composition in free text. The result is the most instructive chart in the project.

ASSETS PER MODEL-CHOSEN STYLE LABEL, 1,417 ASSETSStudio Photography · right-third negative spaceStudio Photography · right-third negative space: 586 assets586Professional Photography · right-third negative spaceProfessional Photography · right-third negative space: 188 assets188Studio Photography · left-third negative spaceStudio Photography · left-third negative space: 73 assets73Studio Photography · right-third copy spaceStudio Photography · right-third copy space: 41 assets41Professional Photography · left-third negative spaceProfessional Photography · left-third negative space: 27 assets27Professional Product Photography · right-thirdProfessional Product Photography · right-third: 22 assets22Industrial Studio Photography · right-thirdIndustrial Studio Photography · right-third: 20 assets20259 other strings259 other strings: 460 assets460
Style labels chosen by the language model across 1,417 assets in the 2025 rewrite. One string accounts for 41 percent of assets and two for 55 percent; the 259 remaining “distinct” labels are mostly paraphrases of the same two ideas. A model asked to pick a style picks its mode, plus lexical noise.

That is the argument for an enumerated style library, made by the alternative. Variety has to be designed in as a draw from a list the model does not control; asked to be creative, it converges. The 72-style library is back in the current version, held as a dictionary separate from the core instructions so the core carries no vendor-specific phrasing and the image model can be swapped as a configuration edit.

Unit economics as the primary metric

Every asset costs a few cents and a few model calls, and the pipeline reports its own economics at the end of every run: tokens, cost per image, and a projection table for larger batches. Across the rewrite's recorded runs the image model was 96 percent of cost and the language model under 4 percent, which is what justified moving image generation to a cheaper model and cut the blended cost per asset by about a third. It also revealed a quieter number: a quarter of sampled keywords produced nothing, almost entirely because the language model under-delivered the keyword count and the validator rejected the row before the padding logic could have repaired it. The failure was invisible in the shipped artifacts and only visible in the run summaries.

The metric that matters is still downloads per asset, not assets per hour, and the pipeline's manifest carries the source keyword and style on every row so sales reports can be joined back to what motivated each image. That join is the feedback loop the whole design points at. I will say plainly that the instrumentation was there and the outcome data was never pulled back in, which is a common gap and the next thing to close.

The design decisions, the four-model call chain, and the reason upload stays a manual review step are in the AI Stock Image Factory case study. If you take one thing from this: instrument the boring part. The keyword sampler and the normalize_metadata function did the work; the model that was supposed to be clever did nothing for a year and nobody noticed.

Book a 30-min callEmail about this work