Quick Read (beta)
Evaluating the State of Semantic Code Search
Semantic code search is the task of retrieving relevant code given a natural language query. While related to other information retrieval tasks, it requires bridging the gap between the language used in code (often abbreviated and highly technical) and natural language more suitable to describe vague concepts and ideas.
We hope that CodeSearchNet Challenge encourages researchers and practitioners to study this interesting task further and will host a competition and leaderboard to track the progress on the challenge. We are also keen on extending CodeSearchNet Challenge to more queries and programming languages in the future.
The deep learning revolution has fundamentally changed how we approach perceptive tasks such as image and speech recognition and has shown substantial successes in working with natural language data. These have been driven by the co-evolution of large (labelled) datasets, substantial computational capacity, and a number of advances in machine learning models.
However, deep learning models still struggle on highly structured data. One example is semantic code search: while search on natural language documents and even images has made great progress, searching code is often still unsatisfying. Standard information retrieval methods do not work well in the code search domain, as there is often little shared vocabulary between search terms and results (e.g. consider a method called deserialize_JSON_obj_from_stream that may be a correct result for the query “read JSON data”). Even more problematic is that evaluating methods for this task is extremely hard, as there are no substantial datasets that were created for this task; instead, the community tries to make do with small datasets from related contexts (e.g. pairing questions on web forums to code chunks found in answers).
To tackle this problem, we have defined the CodeSearchNet Challenge on top of a new CodeSearchNet Corpus. The CodeSearchNet Corpus was programmatically obtained by scraping open-source repositories and pairing individual functions with their (processed) documentation as natural language annotation. It is large enough (2 million datapoints) to enable training of high-capacity deep neural models on the task. We discuss this process in detail in section 2 and also release the data preprocessing pipeline to encourage further research in this area.
Finally, we create a number of baseline methods using a range of state-of-the-art neural sequence processing techniques (bag of words, RNNs, CNNs, attentional models) and evaluate them on our datasets. We discuss these models in section 4 and present some preliminary results.
2. The Code Search Corpus
As it is economically infeasible to create a dataset large enough for training high-capacity models using expert annotations, we instead create a proxy dataset of lower quality. For this, we follow other attempts in the literature (barone2017parallel; gu2018deep; fernandes2018structured; cambronero2019deep) and pair functions in open-source software with the natural language present in their respective documentation. However, to do so requires a number of preprocessing steps and heuristics. In the following, we discuss some general principles and decisions driven by in-depth analysis of common error cases.
CodeSearchNet Corpus Collection
To generate training data for the CodeSearchNet Challenge, we first consider only those functions in the corpus that have documentation associated with them. This yields a set of pairs where is some function documented by . To make the data more realistic proxy for code search tasks, we then implement a number of preprocessing steps:
Documentation is truncated to the first full paragraph, to make the length more comparable to search queries and remove in-depth discussion of function arguments and return values.
Pairs in which is shorter than three tokens are removed, since we do not expect such comments to be informative.
Functions whose implementation is shorter than three lines are removed, these often include unimplemented methods, getters, setters, etc.
Functions whose name contains the substring “test” are removed. Similarly, we remove constructors and standard extension methods such as __str__ in Python or toString in Java.
We remove duplicates from the dataset by identifying (near) duplicate functions and only keeping one copy of them (we use the methods described in lopes2017dejavu; allamanis2018adverse). This removes multiple versions of auto-generated code and cases of copy & pasting.
The filtered corpus and the data extraction code are released at https://github.com/github/CodeSearchNet.
The resulting dataset contains about 2 million pairs of function-documentation pairs and about another 4 million functions without an associated documentation (Table 1). We split the dataset in 80-10-10 train/valid/test proportions. We suggest that users of the dataset employ the same split.
|Number of Functions|
|Go||347 789||726 768|
|Java||542 991||1 569 889|
|PHP||717 313||977 821|
|Python||503 502||1 156 085|
|Ruby||57 393||164 048|
|All||2 326 976||6 452 446|
Unsurpsingly, the scraped dataset is quite noisy. First, documentation is fundamentally different from queries, and hence uses other forms of language. It is often written at the same time and by the same author as the documented code, and hence tends to use the same vocabulary, unlike search queries. Second, despite our data cleaning efforts we are unable to know the extent to which each documentation accurately describes its associated code snippet For example, a number of comments are outdated with regard to the code that they describe. Finally, we know that some documentation is written in other languages, whereas our CodeSearchNet Challenge evaluation dataset focuses on English queries.
3. The Code Search Challenge
To evaluate on the CodeSearchNet Challenge, a method has to return a set of relevant results from CodeSearchNet Corpus for each of 99 pre-defined natural language queries. Note that the task is somewhat simplified from a general code search task by only allowing full functions/methods as results, and not arbitrary chunks of code.11 1 Note that on a sufficiently large dataset, this is not a significant restriction: more commonly implemented functionality almost always appears factored out into a function somewhere. The CodeSearchNet Challenge evaluation dataset consists of the 99 queries with relevance annotations for a small number of functions from our corpus likely to be returned. These annotations were collected from a small set of expert programmers, but we are looking forward to widening the annotation set going forward.
To ensure that our query set is representative, we obtained common search queries from Bing that had high click-through rates to code and combined these with intent rewrites in StaQC (yao2018staqc). We then manually filtered out queries that were clearly technical keywords (e.g. the exact name of a function such as tf.gather_nd) to obtain a set of 99 natural language queries. While most of the collected queries are generic, some of them are language-specific.
Obviously, we cannot annotate all query/function pairs. To filter this down to a more realistically-sized set, we used our implementations of baseline methods and ensembled them (see section 4) to generate 10 candidate results per query and programming language. Concretely, we used ensembles of all neural models and ElasticSearch to generate candidate results, merge the suggestions and pick the top 10. We used a simple web interface for the annotation process. The web interface firsts shows instructions (see Figure 1) and then allows the annotator to pick a programming language. Then, one query/function pair is shown at a time, as shown in Figure 2. A link to the origin of the shown function is included, as initial experiments showed that some annotators found inspecting the context of the code snippet helpful to judge relevance. The order of query/code pairs shown to the user is randomized but weakly ordered by the number of expert annotations already collected. Annotators are unlikely to see several results for the same query unless they handle many examples. By randomizing the order, we aim to allow users to score the relevance of each pair individually without encouraging comparisons of different results for the same query.
We collected 4 026 annotations across six programming languages and prioritized coverage over multiple annotations per query-snippet pair. Our annotators are volunteers with software engineering, data science and research roles and were asked to only annotate examples for languages they had significant experience with. This led to a skewed distribution of annotations w.r.t. the considered programming languages.
|Count by Relevance Score||Total|
For the 891 query-code pairs where we have more than one annotation, we compute the squared Cohen’s kappa interannotator agreement to estimate the quality of the task. The agreement is moderate with Cohen . This is somewhat expected given that this task was relatively open-ended, as we will discuss next.
During the annotation process we made some observations in discussions with the annotators and through the notes they provided in the web interface (see Fig. 2). These comments point to some general issues in implementing code search:
- Code Quality:
A subset of the results being returned are functionally correct code, but of low quality, even though they originated in reasonably popular projects. In this context, low quality refers to unsatisfactory readability, bad security practices, known antipatterns and potentially slow code. Some annotators felt the need to give lower relevance scores to low-quality code as they would prefer not to see such results.
- Query Ambiguity:
Queries are often ambiguous without additional context. For example, the query “how to determine if a string is a valid word” can have different correct interpretations depending on the domain-specific meaning of “valid word”.
- Library vs. Project Specific:
Often a search yields code that is very specific to a given project (e.g. using internal utility functions), whereas other times the code is very general and verbose (e.g. containing code that could be factored out). Which of these is preferable depends on the context of the query, which we did not explicitly specify when asking for annotations.
Some results were semantically correct, but not relying on related helper functions and thus not self-contained. Some annotators were uncertain if such results should be considered relevant.
A common problem in results were functions implementing the inverse functionality of the query, e.g. “convert int to string” would be answered by stringToInt. This suggests that the baseline models used for pre-filtering have trouble with understanding such semantic aspects.
3.1. Evaluation of Ranking Models
To track the progress on the CodeSearchNet Challenge we have deployed a Weights & Biases leaderboard at https://app.wandb.ai/github/codesearchnet/benchmark. We hope that this leaderboard will allow the community to better compare solutions to the code search task.
We used normalized discounted cumulative gain (NDCG) to evaluate each competing method. NDCG is a commonly used metric (manning2008introduction) in information retrieval. We compute two variants of NDCG: (a) NDCG computed over the subset of functions with human annotations (“Within”) (b) NDCG over the whole CodeSearchNet Corpus (“All”). We make this distinction as the NDCG score computed over the whole corpus may not necessarily represent the quality of a search tool, as a new tool may yield relevant but not-annotated functions.
4. Baseline CodeSearch models
We implemented a range of baseline models for the code search task, using standard techniques from neural sequence processing and web search.
4.1. Joint Vector Representations for Code Search
Following earlier work (gu2018deep; mitra2018introduction), we use joint embeddings of code and queries to implement a neural search system. Our architecture employs one encoder per input (natural or programming) language and trains them to map inputs into a single, joint vector space. Our training objective is to map code and the corresponding language onto vectors that are near to each other, as we can then implement a search method by embedding the query and then returning the set of code snippets that are “near” in embedding space. Although more complex models considering more interactions between queries and code can perform better (mitra2018introduction), generating a single vector per query/snippet allows for efficient indexing and search.
To learn these embedding functions, we combine standard sequence encoder models in the architecture shown in Figure 3. First, we preprocess the input sequences according to their semantics: identifiers appearing in code tokens are split into subtokens (i.e. a variable camelCase yields two subtokens camel and case), and natural language tokens are split using byte-pair encoding (BPE) (gage1994new; sennrich2016neural).
Then, the token sequences are processed to obtain (contextualized) token embeddings, using one of the following architectures.
- Neural Bag of Words:
where each (sub)token is embedded to a learnable embedding (vector representation).
- Bidirectional RNN models:
where we employ the GRU cell (cho2014properties) to summarize the input sequence.
- 1D Convolutional Neural Network:
over the input sequence of tokens (kim2014convolutional).
where multi-head attention (vaswani2017attention) is used to compute representations of each token in the sequence.
The token embeddings are then combined into a sequence embedding using a pooling function, for which we have implemented mean/max-pooling and an attention-like weighted sum mechanism. For all models, we set the dimensionality of the embedding space to 128.
During training we are given a set of pairs of code and natural language descriptions and have instantiated a code encoder and a query encoder . We train by minimizing the loss
i.e. maximize the inner product of the code and query encodings of the pair, while minimizing the inner product between each and the distractor snippets (). Note that we have experimented with other similar objectives (e.g. considering cosine similarity and max-margin approaches) without significant changes in results on our validation dataset. The code for the baselines can be found at https://github.com/github/CodeSearchNet.
At test time, we index all functions in CodeSearchNet Corpus using Annoy. Annoy offers fast, approximate nearest neighbor indexing and search. The index includes all functions in the CodeSearchNet Corpus, including those that do not have an associated documentation comment.
4.2. ElasticSearch Baseline
In our experiments, we additionally included ElasticSearch, a widely used search engine with the default parameters. We configured it with an index using two fields for every function in our dataset: the function name, split into subtokens; and the text of the entire function. We use the default ElasticSearch tokenizer.
Following the training/validation/testing data split, we train our baseline models using our objective from above. While it does not directly correspond to the real target task of code search, it has been widely used as a proxy for training similar models (cambronero2019deep; yao2019coacor).
|Encoder||CodeSearchNet Corpus (MRR)|
For testing purposes on CodeSearchNet Corpus, we fix a set of 999 distractor snippets for each test pair and test all trained models. Table 3 presents the Mean Reciprocal Rank results on this task. Overall, we see that the models achieve relatively good performance on this task, with the self-attention-based model performing best. This is not unexpected, as the self-attention model has the highest capacity of all considered models.
We have also run our baselines on CodeSearchNet Challenge and show the results in Table 4. Here, the neural bag of words model performs very well, whereas the stronger neural models on the training task do less well. We note that the bag of words model is particularly good at keyword matching, which seems to be a crucial facility in implementing search methods. This hypothesis is further validated by the fact that the non-neural ElasticSearch-based baseline performs the best among all baselines models we have tested. As noted by cambronero2019deep, this can be attributed to the fact that the training data constructed from code documentation is not a good match for the code search task.
|Encoder||CodeSearchNet Challenge– NDCG Within||CodeSearchNet Challenge– NDCG All|
5. Related Work
Applying machine learning to code has been widely considered (allamanis2018survey). A few academic works have looked into related tasks. First, semantic parsing has received a lot of attention in the NLP community. Although most approaches are usually aimed towards creating an executable representation of a natural language utterance with a domain-specific language, general-purpose languages have been recently considered by yin2017syntactic; ling2016latent; hashimoto2018retrieve; lin2018nl2bash.
iyer2018mapping generate code from natural language within the context of existing methods, whereas allamanis2016convolutional; alon2018code2seq consider the task of summarizing functions to their names. Finally, fernandes2018structured consider the task of predicting the documentation text from source code.
More related to CodeSearchNet is prior work in code search with deep learning. In the last few years there has been research in this area ( yao2019coacor; gu2018deep; gu2016deep; cambronero2019deep), and architectures similar to those discussed previously have been shown to work to some extent. Recently, cambronero2019deep looked into the same problem that CodeSearchNet is concerned with and reached conclusions similar to those discussed here. In contrast to the aforementioned works, here we provide a human-annotated dataset of relevance scores and test a few more neural search architectures along with a standard information retrieval baseline.
6. Conclusions & Open Challenges
We hope that CodeSearchNet is a good step towards engaging with the machine learning, IR and NLP communities towards developing new machine learning models that understand source code and natural language. Despite the fact this report gives emphasis on semantic code search we look forward to other uses of the presented datasets. There are still plenty of open challenges in this area.
Our ElasticSearch baseline, that performs traditional keyword-based search, performs quite well. It has the advantage of being able to efficiently use rare terms, which often appear in code. Researching neural methods that can efficiently and accurately represent rare terms will improve performance.
Code semantics such as control and data flow are not exploited explicitly by existing methods, and instead search methods seem to be mainly operate on identifiers (such as variable and function) names. How to leverage semantics to improve results remains an open problem.
Recently, in NLP, pretraining methods such as BERT (devlin2018bert) have found great success. Can similar methods be useful for the encoders considered in this work?
Our data covers a wide range of general-purpose code queries. However, anecdotal evidence indicates that queries in specific projects are usually more specialized. Adapting search methods to such use cases could yield substantial performance improvements.
Code quality of the searched snippets was a recurrent issue with our expert annotators. Despite its subjective nature, there seems to be agreement on what constitutes very bad code. Using code quality as an additional signal that allows for filtering of bad results (at least when better results are available) could substantially improve satisfaction of search users.