User:EBernhardson (WMF)/Notes/Accept-Language
November 2015 - Questions and comments are welcome on the talk page
- This is currently a work in progress and is not complete*
Hypothesis
[edit]Using the first non-English Accept-Language HTTP header will provide a good proxy for the language the query is in when the query returns no results against the wiki it was already run against. Further that this is a better proxy than the existing elasticsearch langdetect plugin.
Final Results
[edit]180,298 full text desktop queries to enwiki for the hour 28,929 filtered to those with a usable non-English accept-language header 2,015 number of those queries that give zero results 395 number of those queries which convert to non-zero results via accept-language 21% conversion rate of zero result with usable accept-language header to non-zero result
21,022 Estimated number of full text desktop queries to enwiki for the hour that have zero results 1.9% conversion rate from zero result to having result for full data set
Comparison to language detector:
2,015 number of zero result queries from above 1,429 number of those that detect to non-english 128 number of those which convert to non-zero results via language detection 9% conversation rate of zero result with usable accept-language header to non-zero result via lang-detect
Generally this looks to imply we should prefer the accept-language header over language detection for choosing a second wiki to query. We will still need to use language detection as only 16% of queries had a usable accept-language header.
Caveats
[edit]This only analyzed traffic to enwiki. It is likely if we ran this for traffic going to ruwiki or zhwiki we would get slightly different results. Additionally this only considers desktop search. Query rewriting for the API is not done by default, but is instead hidden behind a feature flag. As such even if we could have an effect on API requests most of them do not enable the rewrite flag and will not be affected. Note that the API makes up something like 75% of all search requests.
Additionally this data set, once filtered to queries with non-English accept headers, has a zero result rate of only 7%. This is 40% lower ((11.6-6.9)/11.6) than the overall zero result rate recorded in our CirrusSearchRequestSet logs for the same hour. Not sure if this means anything, but seems like a large variance.
Process
[edit]Extract one hour worth of desktop full text searches from webrequest logs
[edit]Started by taking an hour worth of queries + accept language headers for enwiki from hive wmf.webrequests table using the following query. The specific day and hour to work with was arbitrarily chosen. This gives us a set of 180,298 queries to start with, which we will use to calculate the expected change to zero result rate. This feels much too low to be the total number of full text queries on enwiki for that hour, but is probably a reasonable number to run this test against.
INSERT OVERWRITE LOCAL DIRECTORY '/home/ebernhardson/hive'
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
STORED AS TEXTFILE
SELECT accept_language, query_string['search']
FROM (SELECT accept_language, str_to_map(substr(uri_query, 2), '&', '=') as query_string
FROM wmf.webrequest
WHERE year = 2015 AND month = 11 AND day = 04 AND hour = 04
AND uri_host = 'en.wikipedia.org'
AND uri_path <> '/w/api.php') x
WHERE length(query_string['search']) > 0
AND (query_string['title'] = 'Special:Search' OR query_string['title'] = 'Special%3ASearch');
Filter out requests with only English or invalid accept-language
[edit]The result of the above query was then run through the following php script to filter out queries that had an invalid accept-language header recorded, or only included English. Run against the above set of 180,298 queries we end up with 28,929 queries that could be affected. This means around 16% of our search queries to enwiki contain a non-English Accept-Language header.
<?php
$data = array();
while( false !== ( $line = fgets( STDIN ) ) ) {
list( $acceptLang, $term ) = explode( "\t", rtrim( $line, "\n" ) );
$term = trim( $term );
if ( strlen( $term ) === 0 ) {
continue;
}
$parsed = parseAcceptLang( $acceptLang );
foreach ( array_keys( $parsed ) as $lang ) {
if ( substr( $lang, 0, 2 ) !== 'en' ) {
$data[] = array( $acceptLang, $term );
break;
}
}
}
usort( $data, function( $a, $b ) {
return strcmp( $a[1], $b[1] );
} );
foreach ( $data as $value ) {
echo implode( "\t", $value ) . "\n";
}
Run the queries through enwiki to find which are zero result queries
[edit]These queries are then run against the enwiki index we have in the hypothesis-testing cluster to see which are zero result queries. This was done with the following command line:
cat queries.accept-lang.sorted | \
ssh -o Compression=yes suggesty.eqiad.wmflabs 'sudo -u vagrant mwscript extensions/CirrusSearch/maintenance/runSearch.php --wiki=enwiki --decode --options='\''{"wgCirrusSearchEnableAltLanguage":false}'\' | \
pv -l -s $(wc -l queries.accept-lang.sorted | awk '{print $1}') \
> queries.accept-lang.sorted.results
The results of this were filtered down to only the zero result queries, giving only 2,015 queries to test our original hypothesis against. Note that 2,015 out of 28,929 means this set had a zero result rate of 6.9% which is much lower than expected. I queried the same hour from the CirrusSearchRequestSet in hive and came up with a ZRR of 11.7% (see appendix for the query used).
cat queries.accept-lang.sorted.results | \
jq -c 'if (.totalHits > 0) . else empty end' |\
wc -l
Sort zero result queries into a bucket per target wiki
[edit]For the next step I needed a map from the languages to their wikis. This was sourced from this gerrit patch.
These queries were then separated out into a file per wiki using the following php script.
<?php
$langMap = include __DIR__ . '/langs.php';
$queryFile = fopen( $argv[1], "r" );
$resultFile = fopen( $argv[2], "r" );
$match = $total = $zeroResult = $hasAcceptLang = $error = 0;
while ( !feof( $queryFile ) && !feof( $resultFile ) ) {
$line = rtrim( fgets( $queryFile ), "\n" );
list( $accept, $encodedQuery ) = explode( "\t", $line, 2 );
$query = urldecode( $encodedQuery );
// not sure why this is necessary, there is some sort of bug in runSearch.php
// most likely, but a quick review didn't turn anything up.
while ( $query === "0" ) {
$line = rtrim( fgets( $queryFile ), "\n" );
list( $accept, $encodedQuery ) = explode( "\t", $line, 2 );
$query = urldecode( $encodedQuery );
}
$result = json_decode( rtrim( fgets( $resultFile ), "\n" ), true );
$total++;
if ( $query !== $result['query'] ) {
continue;
}
$match++;
if ( isset( $result['error'] ) ) {
$error++;
continue;
}
// totalHits will be not set for empty queries, such as ' '
if ( !isset( $result['totalHits'] ) || $result['totalHits'] > 0 ) {
continue;
}
$zeroResult++;
// we now have a query and know it's a zero result, strip the accept
// language header down to the first accepted language that is not
// english
$parsedAcceptLang = array_keys( parseAcceptLang( $accept ) );
$tryWiki = null;
foreach ( $parsedAcceptLang as $lang ) {
$shortLangCode = preg_replace( '/-.*$/', '', $lang );
if ( isset( $langMap[$shortLangCode] ) ) {
$tryWiki = $langMap[$shortLangCode];
break;
}
}
if ( $tryWiki === null ) {
continue;
}
file_put_contents( $tryWiki, urlencode( $query ) . "\n", FILE_APPEND );
$hasAcceptLang++;
}
fwrite( STDERR, "\nMatch: $match\nTotal: $total\nError: $error\nZero Result: $zeroResult\nHas accept-language: $hasAcceptLang\n");
A quick look at which wikis the queries were assigned to was done with
wc -l * | sort -rn | head -n 21 | tail -n 20
This gives the following top 20 targets of queries from enwiki in the analyzed hour:
545 zhwiki 329 kowiki 241 svwiki 220 eswiki 114 jawiki 53 dewiki 51 ruwiki 50 arwiki 45 thwiki 44 ptwiki 43 frwiki 33 hiwiki 32 idwiki 19 viwiki 17 mswiki 14 hewiki 13 plwiki 12 nlwiki 11 fiwiki 10 trwiki
I don't have the resources available to run the full 180k query set to calculate it's ZRR rate, but we can estimate using #Calculate_zero_result_rate_from_hive_CirrusSearchRequestSet_table. This gives a ZRR of 11.7%, which suggests 21k of the 180k queries would have zero results. We were able to convert 395 of those 21k queries into results for a conversion rate of 1.9%
Search target wikis
[edit]Now that we have all the zero result queries that have a usable accept-language header broken out into files per wiki we can run them with the following:
for i in $(wc -l * | sort -rn | head -n 21 | tail -n 20 | awk '{print $2}'); do
cat $i | \
ssh -o Compression=yes suggesty.eqiad.wmflabs 'sudo -u vagrant mwscript extensions/CirrusSearch/maintenance/runSearch.php --wiki='$i' --decode --options='\''{"wgCirrusSearchEnableAltLanguage":false}'\' | \
pv -l -s $(wc -l $i | awk '{print $1}') \
> $i.results
done
These can then be processed to get the new zero result rate:
(for i in *.results; do
TOTAL="$(wc -l < $i)"
NONZERO="$(cat $i | jq -c 'if (.totalHits > 0) then . else empty end' | wc -l)"
echo $i total: $TOTAL non-zero percent: $(echo "scale=3; $NONZERO / $TOTAL" | bc)
done) | sort -rnk3
Which results in:
zhwiki.results total: 545 non-zero percent: .216 kowiki.results total: 329 non-zero percent: .079 svwiki.results total: 241 non-zero percent: .522 eswiki.results total: 220 non-zero percent: .222 jawiki.results total: 114 non-zero percent: .140 dewiki.results total: 53 non-zero percent: .207 ruwiki.results total: 51 non-zero percent: .235 arwiki.results total: 50 non-zero percent: .120 thwiki.results total: 45 non-zero percent: .066 ptwiki.results total: 44 non-zero percent: .136 frwiki.results total: 43 non-zero percent: .162 hiwiki.results total: 33 non-zero percent: 0 idwiki.results total: 32 non-zero percent: .281 viwiki.results total: 19 non-zero percent: 0 mswiki.results total: 17 non-zero percent: .058 hewiki.results total: 14 non-zero percent: 0 plwiki.results total: 13 non-zero percent: .076 nlwiki.results total: 12 non-zero percent: .083 fiwiki.results total: 11 non-zero percent: .090 trwiki.results total: 10 non-zero percent: .200
Across the top 20 wikis there are 1896 queries, and 395 were able to find results. This gives an overall 20.8% conversion rate. Considering the full set of queries, including those against languages we did not run due to being in the long tail, we have 395/2011 = 19.6%.
Compare against language detection
[edit]To determine if language detection does a better job than the accept-language header we take the set of 1896 queries that were made above and re-bucket them based on language detection. That was done with the following code:
$errors = $undetectable = $detected = $unknown = 0;
while ( false !== ( $line = fgets( STDIN ) ) ) {
$encodedQuery = rtrim( $line, "\n" );
$lang = detectLanguage( $encodedQuery );
if ( $lang === null ) {
$undetectable++;
continue;
}
$lang = preg_replace( '/-.*$/', '', $lang );
if ( isset( $langs[$lang] ) ) {
file_put_contents( $langs[$lang], $line, FILE_APPEND );
$detected++;
} else {
// usually 'en'
$unknown++;
}
}
fwrite( STDERR, "Errors: $errors\nUndetectable: $undetectable\nUnknown: $unknown\nDetected: $detected\n" );
// taken from CirrusSearch\Searcher::detectLanguage()
function detectLanguage( $encodedText ) {
$ch = curl_init( 'http://localhost:9200/_langdetect' );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $encodedText );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
$value = json_decode( curl_exec( $ch ), true );
if ( $value && !empty( $value['languages'] ) ) {
$langs = $value['languages'];
if ( count( $langs ) === 1 ) {
return $langs[0]['language'];
}
if ( count( $langs ) === 2 ) {
if ( $langs[0]['probability'] > 2*$langs[1]['probability'] ) {
return $langs[0]['language'];
}
}
}
return null;
}
It was run as follows:
mkdir per-wiki.lang-detect
cd per-wiki.lang-detect
cat ../per-wiki.accept-lang/*wiki | php ../lang-detect.php
Re-running the same analysis as above for per-wiki ZRR we get:
ptwiki.results total: 193 non-zero percent: .015 itwiki.results total: 149 non-zero percent: .120 rowiki.results total: 148 non-zero percent: .081 dewiki.results total: 143 non-zero percent: .118 tlwiki.results total: 87 non-zero percent: 0 frwiki.results total: 67 non-zero percent: .119 eswiki.results total: 65 non-zero percent: .307 sqwiki.results total: 57 non-zero percent: 0 idwiki.results total: 53 non-zero percent: .207 huwiki.results total: 47 non-zero percent: 0 svwiki.results total: 40 non-zero percent: .350 ltwiki.results total: 38 non-zero percent: 0 nowiki.results total: 36 non-zero percent: .138 zhwiki.results total: 31 non-zero percent: .129 dawiki.results total: 31 non-zero percent: .032 hrwiki.results total: 29 non-zero percent: .034 fiwiki.results total: 29 non-zero percent: .034 plwiki.results total: 28 non-zero percent: .071 etwiki.results total: 23 non-zero percent: 0 trwiki.results total: 22 non-zero percent: .045 nlwiki.results total: 20 non-zero percent: .250 viwiki.results total: 12 non-zero percent: 0 lvwiki.results total: 7 non-zero percent: 0 cswiki.results total: 7 non-zero percent: .142
This finds results for 124 queries out of 2015, for a conversion rate of 6.1%.
Additional functions used in above scripts
[edit]parseAcceptLang
[edit]// sourced from mediawiki WebRequest::getAcceptLang()
function parseAcceptLang( $acceptLang ) {
if ( !$acceptLang ) {
return array();
}
$acceptLang = strtolower( $acceptLang );
$lang_parse = null;
preg_match_all(
'/([a-z]{1,8}(-[a-z]{1,8})*|\*)\s*(;\s*q\s*=\s*(1(\.0{0,3})?|0(\.[0-9]{0,3})?)?)?/',
$acceptLang,
$lang_parse
);
if ( !count( $lang_parse[1] ) ) {
return array();
}
$langcodes = $lang_parse[1];
$qvalues = $lang_parse[4];
$indices = range( 0, count( $lang_parse[1] ) - 1 );
foreach ( $indices as $index ) {
if ( $qvalues[$index] === '' ) {
$qvalues[$index] = 1;
} elseif ( $qvalues[$index] == 0 ) {
unset( $langcodes[$index], $qvalues[$index], $indices[$index] );
}
}
array_multisort( $qvalues, SORT_DESC, SORT_NUMERIC, $indices, $langcodes );
return array_combine( $langcodes, $qvalues );
}
Calculate zero result rate from hive CirrusSearchRequestSet table
[edit]SELECT SUM(results.outcome) AS non_zero,
COUNT(*) - SUM(results.outcome) AS zero,
1 - SUM(results.outcome) / COUNT(*) AS zero_result_rate
FROM ( SELECT IF(array_sum(requests.hitstotal) > 0, 1, 0) AS outcome
FROM ebernhardson.cirrussearchrequestset
WHERE year=2015 AND month=11 AND day=4 AND hour=4
AND wikiid='enwiki'
AND source='web'
AND array_contains(requests.querytype, 'full_text')
) AS results;
Result:
non_zero zero zero_result_rate 131003 17298 0.11664115548782539