(This page has no text content)
Generative Engine Optimization with Python Data-Driven Methods for LLM Retrieval and Citation With Early Release ebooks, you get books in their earliest form —the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles. Andreas Voniatis
Generative Engine Optimization with Python by Andreas Voniatis Copyright © 2027 Andreas Voniatis, Inc. All rights reserved. Published by O’Reilly Media, Inc., 141 Stony Circle, Suite 195, Santa Rosa, CA 95401. O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (https://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com. Acquisitions Editor: Aaron Black Development Editor: Sara Hunter Production Editor: Christopher Faucher Cover Designer: Susan Brown Cover Illustrator: José Marzan Jr. Interior Designer: David Futato Interior Illustrator: Kate Dullea April 2027: First Edition Revision History for the Early Release 2026-07-30: First release
See https://oreilly.com/catalog/errata.csp?isbn=9798341674585 for release details. The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Generative Engine Optimization with Python, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc. The views expressed in this work are those of the author and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights. 979-8-341-67454-7 [LSI]
Brief Table of Contents (Not Yet Final) Preface (unavailable) Chapter 1: The Shift from SEO to GEO (unavailable) Chapter 2: Topic Research (unavailable) Chapter 3: Content for AI (unavailable) Chapter 4: Technical Optimization for AI Platforms (unavailable) Chapter 5: Building AI-Visible Authority (unavailable) Chapter 6: Measuring GEO Performance (available) Chapter 7: Investigating and Future-Proofing GEO (unavailable)
Chapter 1. Measuring GEO Performance A NOTE FOR EARLY RELEASE READERS With Early Release ebooks, you get books in their earliest form —the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles. This will be the 6th chapter of the final book. Please note that the GitHub repo will be made active later on. If you’d like to be actively involved in reviewing and commenting on this draft, please reach out to the editor at shunter@oreilly.com. This chapter will teach you how to build Python analytical pipelines that track and interpret brand visibility across AI search platforms for your dashboards. You’ll learn how to: Extract GEO raw performance data across multiple platforms Analyze performance metrics aggregated by search intent, platform and overall AI search It’s near impossible to know how your website shows up in AI search results for your target audiences. This chapter outlines the data pipelines to help you build the infrastructure to track
Generative Engine Optimization (GEO) performance to power your dashboards and reporting systems. You can’t reduce AI search visibility to a single metric because it hides the many moving parts that make up the overall score. Tracking AI search visibility successfully requires that you analyze/track(?) two sets of results: Answer: generated in response to user prompts forming the main content of the AI search platform’s response Cited sources: web page results AI search relies upon to summarize when generating the answers. Cited sources is an additional dimension to traditional search engine results which is mainly a list of website page links. Within the generated answer itself, there are two attributes also worth tracking: Position: The rank of your organization within a list when your target audience are prompting for best or recommended organizations in your industry Sentiment: How AI speaks about your organization While rank position has been the mainstay of performance tracking in search engines, sentiment is new and incredibly important. For example, if a user prompted: “Which are companies should I avoid for Oracle Netsuite consulting?” Having your organization as part of the answer would be highly undesirable.
The importance of GEO performance measurement Because the feedback loop between GEO and AI search results can be quite slow, noisy and distributed across multiple AI platforms, having a measurement framework is critical to managing expectations and optimizing campaigns. Especially as AI platforms tend to behave differently from one another. For example, you’ll see in this chapter’s pipeline how ChatGPT doesn’t cite many source website pages when compared with Gemini. The performance measurement framework will help the GEO see how the activities are moving (or not) the metrics on mentions, citations, positions and sentiment. It also allows the GEO to falsify the evolving best practices that are being constantly published online. Coding the performance measurement framework into a pipeline will automate the data extraction and analysis, ready for reports exporting to dashboard, spreadsheets or otherwise. Data Pipelines for Performance Measurement The pipeline starts by querying the APIs of the main AI search platforms. For this walkthrough, I’ve chosen to include the following:, : ChatGPT Gemini Perplexity
Claude You may choose to query other T AI search platforms, such as xAI. Whatever you choose, you’ll need to have an API key which is usually paid under subscription. From the platforms, you’ll extract search responses for mentioned, citations, position, and sentiment metrics. You’ll then aggregate the metrics by search intent, platform and AI overall to get a sense of where the website is under and over performing. Because AI search is generative by nature (i.e. made up), the results are not identical for each prompt so you’ll need to make multiple API requests for each prompt to obtain samples that help you determine the rates for each metric. Data Extraction from AI search APIs Each AI search platform API has python packages which will need to be installed before importing. As a reminder, you’ll need a paid subscription to do this. import os import anthropic from google import genai as google_genai from openai import OpenAI Once imported the API keys are set and instantiated: OPENAI_API_KEY = "sk-proj-..." GEMINI_API_KEY = "..." ANTHROPIC_API_KEY = "sk-ant-..." PERPLEXITY_API_KEY = "pplx-..."
openai_client = OpenAI(api_key=OPENAI_API_KEY) anthropic_client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) genai.configure(api_key=GEMINI_API_KEY) Further imports are made to help manipulate and process the data: import os import re import time import glob from datetime import date from urllib.parse import urlparse import pandas as pd from transformers import pipeline as hf_pipeline Pipeline input constants are set to prevent magic strings being scattered throughout the notebook starting with the identity. Identity The identity section sets the parameters for the website, its target region and the region prompt. The run date will be used for export file naming. YOUR_DOMAIN = "randgroup.com" TARGET_REGION = "United States" REGION_HINT = f"You are answering for a user based in the {TARGET_REGION}." RUN_DATE = date.today().isoformat() Pipeline parameters
For this walkthrough, I’m using 10 samples by way of API requests to be taken per prompt. If the API responses returned faster, then I recommend taking higher sample sizes up to 100. The sleep seconds (SLEEP_SECS) is a per-platform pause between API calls to help with rate limiting, cost control and avoid triggering abuse detection even if the requests are within the rate limits. Sleep second values will vary according to the platform based on previous API usage and the platform’s rate limits. RUNS_PER_QUESTION = 10 # SLEEP_SECS = { "openai": 1.0, "gemini": 2.0, "claude": 0.5, "perplexity": 0.5, } TOP_N_COMPETITORS = 10 Reserved for the visualisation pipeline BRAND_COLOURS = {"primary": "#069492", "secondary": "#29265b", "accent": "#e1ff00"} Brand map The DOMAIN_BRAND_MAP dictionary acts as a lookup table mapping the competitor domains to their brand names used in the AI API response as AI models typically don’t print domain names in response text. The client brand names CLIENT_ALIASES is set by using get on the DOMAIN_BRAND_MAP
DOMAIN_BRAND_MAP = { "buildxact.com": ["buildxact"], "bluecollar.cloud": ["bluecollar", "blue collar"], "getjobber.com": ["jobber"], "plative.com": ["plative"], "accrete.com": ["accrete"], "appficiency.com": ["appficiency"], "randgroup.com": ["rand group", "randgroup"], } CLIENT_ALIASES = DOMAIN_BRAND_MAP.get(YOUR_DOMAIN, ["randgroup", "rand group"]) Target Questions and Competitive Queries The prompts are set in a list assigned to TARGET_QUESTIONS and COMPETITIVE_QUERIES which are categorized for analysis later. TARGET_QUESTIONS covers questions that are typically asked by the target audience during their research phase. TARGET_QUESTIONS = [ # Fragmented data pain points "why is my construction business data spread across so many systems", "how do I get a single view of job costs across my construction trades business", "how do construction trades businesses eliminate spreadsheet dependency", "what causes data silos in construction trades companies", "how do I connect field operations data with my back office in construction", "why do my project managers and accountants see different numbers for the same job",
"how do construction companies get real time visibility into job profitability", "what is the best way to consolidate financial and operational data in construction", "how do I stop losing money because of delayed billing in construction trades", "how do construction trades CEOs get accurate cash flow forecasting", # NetSuite solutions for construction trades "is NetSuite good for construction trades businesses", "best ERP for construction trades companies", "how does NetSuite handle job costing for construction", "can NetSuite manage subcontractor payments for construction trades", "how long does a NetSuite implementation take for a construction company", "NetSuite vs Sage for construction trades", "how do construction trades businesses customise NetSuite for field operations", "what does a NetSuite implementation cost for a mid-size construction company", "how do I know if my construction business is ready for NetSuite", ] COMPETITIVE_QUERIES are commercial where audiences search for a partner to help them resolve their problem. COMPETITIVE_QUERIES = [ "best NetSuite consultants for construction trades", "top ERP implementation partners for
construction companies", "best NetSuite implementation partners USA construction", ] Both sets of questions are merged into a single list. ALL_QUESTIONS = TARGET_QUESTIONS + COMPETITIVE_QUERIES Below are a set of printouts to CLUSTER_MAP = { **{q: "problem" for q in TARGET_QUESTIONS[:10]}, **{q: "solutions" for q in TARGET_QUESTIONS[10:]}, **{q: "brands" for q in COMPETITIVE_QUERIES}, } Below are a set of printouts to verify our inputs before running the API calls: print(f"Total API calls : {len(ALL_QUESTIONS) * RUNS_PER_QUESTION * 4:,} (4 platforms × 10 sessions × {len(ALL_QUESTIONS)} prompts)") print(f"Client domain : {YOUR_DOMAIN}") print(f"Target region : {TARGET_REGION}") print(f"Target questions : {len(TARGET_QUESTIONS)}") print(f"Competitive queries: {len(COMPETITIVE_QUERIES)}") print(f"Runs per question : {RUNS_PER_QUESTION}") print(f"Run date : {RUN_DATE}") print(f"Total API calls : {len(ALL_QUESTIONS) *
RUNS_PER_QUESTION * 4:,} (4 platforms × 10 sessions × {len(ALL_QUESTIONS)} prompts)") Output: Total API calls : 880 (4 platforms × 10 sessions × 22 prompts) Client domain : randgroup.com Target region : United States Target questions : 19 Competitive queries: 3 Runs per question : 10 Run date : 2026-06-11 Total API calls : 880 (4 platforms × 10 sessions × 22 prompts) The output summarizes the number of API calls being made to extract visibility performance data broken down by the number of questions, sessions and platforms. ## ── Helper Functions Query corpus This section will define functions that convert a raw API content response into data structures with extracted URLs, domains, mentions using the brand map defined earlier, and position. def extract_urls(text: str) -> list[str]: return re.findall(r'https?://[^\s\)\]\,\"\'<>]+', text) def normalise_domain(url: str) -> str:
try: parsed = urlparse(url if url.startswith("http") else "https://" + url) return parsed.netloc.lower().removeprefix("www.") except Exception: return "" def get_cited_domains(text: str) -> list[str]: return list({d for u in extract_urls(text) if (d := normalise_domain(u))}) def detect_mentions(text: str, domain_brand_map: dict) -> dict[str, bool]: text_lower = text.lower() return { domain: any( re.search(rf'\b{re.escape(alias)}\b', text_lower) for alias in aliases ) for domain, aliases in domain_brand_map.items() } def position_score(text: str, aliases: list[str]) -> float: text_lower = text.lower() hits = [ m.start() for alias in aliases if (m :=
re.search(rf'\b{re.escape(alias)}\b', text_lower)) ] return round(1.0 - min(hits) / max(len(text), 1), 4) if hits else 0.0 def classify_format(text: str) -> str: if re.search(r'\|\s*[-:]\s*\|', text) or text.count("|") > 4: return "table" if re.search(r'^\s*[-•*]\s+', text, re.MULTILINE): return "list" return "paragraph" Sentiment Classifier (HuggingFace) The next cell is our sentiment classifier which will extract the sentiment from the raw API content response by loading a pretrained sentiment model from HuggingFace to classify the sentences that contain a brand mention. I used HuggingFace as opposed to hard-coded term lists (positive/negative word banks) which are brittle and miss negation, irony, and domain-specific phrasing. The HuggingFace is a fine- tuned transformer that will handle these correctly without manual curation. HuggingFace uses the distilbert-base-uncased-finetuned-sst-2- english model which is lightweight, doesn’t require a GPU and is highly suited to short sentence structures. The classifier runs once per response record and operates only on the sentences that contain a brand alias rather than the full response
text to help keep input length manageable and focused on the brand mention context rather than the entire answer. sentiment_classifier = hf_pipeline( "sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2- english", truncation=True, max_length=512, ) def extract_mention_context(text: str, aliases: list[str]) -> str: """Return the sentences containing any brand alias, concatenated.""" sentences = re.split(r'[.!?]', text) relevant = [ s.strip() for s in sentences if any(re.search(rf'\b{re.escape(a)}\b', s.lower()) for a in aliases) ] return " ".join(relevant) def classify_sentiment(text: str, aliases: list[str]) -> float: context = extract_mention_context(text, aliases) if not context: return 0.0 try: result = sentiment_classifier(context)[0] label = result["label"].lower() score = result["score"] scaled = (score - 0.5) * 2 return round(scaled if label == "positive"
else -scaled, 4) except Exception: return 0.0 print("Sentiment classifier loaded:", sentiment_classifier.model.name_or_path) Output: Loading weights: 100% 104/104 [00:00<00:00, 7976.15it/s] Sentiment classifier loaded: distilbert-base- uncased-finetuned-sst-2-english The output confirms the model is loaded successfully. The function will score sentiment along a scale from -1 (negative) to 1 (positive). The confidence score from the classifier is used to scale the result, so a weakly positive result returns less than 1.0. AI Platform API Functions Query functions are defined per platform (OpenAI, Gemini, Claude, Perplexity). Each accepts a full conversation history and returns the next response plus the updated history, a question string and a conversation history list, returning response text, cited domains, cited URLs, and the updated history. The shared utility functions trim_history and with_retries are defined and used by all four API Functions. Each adapter appends the user question to history, calls the platform API with trim_history(history) as the payload, extracts response text and cited the sources using platform-specific extraction logic before appending the assistant response to history.
Then it returns a dict with response_text, cited_domains, cited_urls, cited_pairs where applicable, and history. On exception, the user message is popped from history before returning an empty result so history remains consistent for subsequent questions. The API keys for each platform are set below. openai_client = OpenAI(api_key=OPENAI_API_KEY) anthropic_client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) google_client = google_genai.Client(api_key=GEMINI_API_KEY) perplexity_client = OpenAI(api_key=PERPLEXITY_API_KEY, base_url="https://api.perplexity.ai") Cap the history at 6 user/assistant pairs to prevent token costs and latency growing across 23 prompts. MAX_HISTORY_TURNS = 6 Returns the last MAX_HISTORY_TURNS * 2 messages, bounding the API payload regardless of session length. def trim_history(history: list[dict]) -> list[dict]: return history[-(MAX_HISTORY_TURNS * 2):] Retries a function up to 3 times with exponential backoff on 429 rate limit errors. def with_retries(fn, retries: int = 3, backoff: int = 30): for attempt in range(retries): try:
Loading comments...
Reply to Comment
Edit Comment